[ 
https://issues.apache.org/jira/browse/NIP-36?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Bob Paulin updated NIP-36:
--------------------------
    Description: 
h2. Motivation

Some Connector configuration properties describe decisions that cannot be 
safely changed after the Connector has started producing data. Typical examples:
 * The output format written to a destination. Once objects have been written 
to an S3 bucket as Parquet, silently switching the property to Avro produces a 
bucket whose contents can no longer be read by a single reader.

 * The physical destination identifier. The S3 bucket, Kafka topic, or 
object-store prefix that the Connector writes into. Changing this after data 
has been written silently forks the destination and leaves the previously 
written data orphaned.

 * The identity used to track source progress. The Kafka consumer group id or 
source offset-tracking key. Changing this after commit either re-reads 
already-processed data or leaves committed offsets behind, in both cases 
producing duplicates or gaps.

Today there is no first-class way for a Connector author to protect such a 
property. The intent lives only in ad hoc validation code inside each 
Connector, is not exposed to the REST API, is not visible in the extension 
manifest, and is not visible in the UI. Users can freely edit these fields in 
the configuration dialog and see a generic error only when they attempt to 
apply the change, if the Connector happens to defend it at all.

These decisions are not permanent, though. A user may deliberately move to a 
new bucket, switch formats, or replay a topic from the beginning. Today the 
only safe way to do that is to delete the Connector and create a new one, 
losing the rest of its configuration and giving the Connector no chance to 
clean up what the old value left behind.
h2. Scope

Out of scope:

Lockable properties for Processors or Controller Services are not part of the 
scope of this NIP. Processors and Controller Services have done without this 
feature and have implemented ways to handle property value changes within the 
component code. Connectors integrate an entire flow worth of components 
together, so forcing Connectors to handle any possible Connector property 
change may not always be possible.

{{nifi-api}} additions:
 * {{ConnectorPropertyDescriptor.isLockable()}} and 
{{{}ConnectorPropertyDescriptor.Builder.lockable(boolean){}}}, defaulting to 
{{{}false{}}}. {{Builder.from(other)}} copies the flag.

 * {{VersionedConnector.lockedProperties}} with getter and setter.

 * {{UnlockContext}} and {{{}Connector.prepareForUnlock(UnlockContext){}}}, 
with a default implementation.

 * {{XmlConnectorDocumentationWriter.writeConnectorProperty(...)}} emits a new 
{{<lockable>}} element.

Framework additions:
 * {{ConnectorNode}} methods to read and restore the locked set, and 
{{{}unlock(...){}}}.

 * {{StandardConnectorNode}} records locks, enforces them, and advertises an 
{{UNLOCK}} action through {{{}getAvailableActions(){}}}.

 * {{VersionedComponentFlowMapper.mapConnector(...)}} persists the locked set, 
and {{StandardConnectorRepository.createConnectorNode(VersionedConnector)}} 
restores it.

Model, DTO, manifest, and C2 plumbing:
 * {{ConnectorPropertyDescriptorDTO}} ({{{}nifi-client-dto{}}}) gains 
{{{}lockable{}}}, populated by 
{{{}DtoFactory.createConnectorPropertyDescriptorDto(...){}}}.

 * {{ConnectorDTO}} ({{{}nifi-client-dto{}}}) gains {{{}lockedProperties{}}}, 
populated by {{{}DtoFactory{}}}.

 * {{org.apache.nifi.extension.manifest.ConnectorProperty}} and 
{{org.apache.nifi.c2.protocol.component.api.ConnectorPropertyDescriptor}} gain 
{{{}lockable{}}}. 
{{StandardRuntimeManifestBuilder.getConnectorPropertyDescriptor(...)}} copies 
it through.

 * The Connector REST resource gains an unlock endpoint, following the existing 
Connector action endpoints.

UI additions ({{{}nifi-frontend{}}}):
 * The Connector property editor shows lock status, and offers unlock and reset 
controls.

h2. Description
h3. Descriptor contract

{{lockable}} is a static, author-declared strategy on the shared 
{{{}ConnectorPropertyDescriptor{}}}. It does not describe the state of any 
Connector instance. A lockable property behaves like any other property until 
the Connector successfully transitions to {{{}RUNNING{}}}, so initial 
configuration, verification, and staged flow deployment are unaffected.
h3. Lock status

Each Connector instance keeps a persisted set of locked property identities, 
exposed as {{{}lockedProperties{}}}. An identity is a configuration step name 
plus a property name; {{ConfigurationStep.Builder}} already requires property 
names to be unique within a step.

A property is LOCKED when its identity is in the set and its current descriptor 
is {{{}lockable{}}}. Otherwise it is UNLOCKED.

On every successful transition to {{{}RUNNING{}}}, the framework adds every 
property whose current descriptor is {{{}lockable{}}}, locking the value in the 
active configuration at that moment. No other state transition adds or removes 
locks, including entering or leaving {{{}TROUBLESHOOTING{}}}. Unlock is the 
only operation that removes identities.
h3. Enforcement

A single private helper, 
{{{}StandardConnectorNode.verifyLockedPropertiesUnchanged(...){}}}, is called 
from the two mutation entry points a user request can reach:
 * {{{}setConfiguration(String, StepConfiguration){}}}: the merge path used by 
REST configuration updates and by {{{}ConnectorTestRunner.configure(...){}}}.

 * {{{}replaceWorkingConfiguration(...){}}}: the whole-configuration 
replacement path used by configuration providers.

For each LOCKED property, the helper compares the incoming raw 
{{ConnectorValueReference}} with the active one, so asset and secret references 
are compared by identity, not by resolved value. An equal reference is 
accepted. A different reference throws {{FlowUpdateException}} naming the 
property, the step, and the Connector.

Restore paths (flow deserialization, snapshot inheritance, 
{{{}recreateWorkingFlowContext{}}}) bypass the check because they reconstruct 
existing state.

!image-2026-10-16-07-49-01-251.png!
h3. Unlock and reset

{{UNLOCK}} is a framework Connector action alongside existing actions such as 
{{START}} and {{{}PURGE_FLOWFILES{}}}. It is allowed when the Connector is 
stopped, is not in Troubleshooting, and has at least one LOCKED property. 
Otherwise it is listed as disallowed with a reason.

The request carries an optional set of property identities:
 * No identities resets the Connector: every LOCKED property is unlocked, and 
{{UnlockContext.isReset()}} is true.

 * A non-empty set unlocks only those identities. If any identity is not 
currently LOCKED, the request fails and nothing is removed.

Before changing the set, the framework calls 
{{{}Connector.prepareForUnlock(UnlockContext){}}}. The locked values and the 
active flow are still in place. {{UnlockContext}} provides the identities, the 
active {{ConnectorValueReference}} for each, the active flow context, and 
{{{}isReset(){}}}.

The Connector uses this hook to drop committed offsets, abandon a destination, 
or refuse the change. It refuses synchronously by throwing 
{{{}FlowUpdateException{}}}, or asynchronously by completing the returned 
{{CompletableFuture}} exceptionally. Either keeps every lock. The framework 
cancels the future if the caller cancels the request, so delegated work must be 
cooperatively cancellable, and the future must not complete while other work 
can still mutate the flow. The default implementation returns a completed 
future.

When the future completes normally, the framework removes the identities and 
persists the Connector. Active values do not change. The user then edits the 
properties through the normal configuration path, so validation, 
{{{}onConfigurationStepConfigured{}}}, {{{}prepareForUpdate{}}}, and 
{{applyUpdate}} all still apply.

 

The next successful start locks them again.

 
h3. Persistence and upgrades

{{VersionedComponentFlowMapper}} writes the locked set to 
{{{}VersionedConnector{}}}. {{StandardConnectorRepository}} restores it before 
the Connector accepts user requests, so enforcement is active immediately after 
restart. The set, including the result of a partial unlock, survives restarts, 
flow imports, and Connector version upgrades.

Because locks are tracked per property:
 * A property that becomes lockable only in a later Connector version stays 
editable until the next successful start.

 * An identity whose current descriptor is not lockable is ignored. Removing 
{{lockable}} in a later version is how an author retires the constraint.

 * A renamed property is a new identity and locks on the next successful start. 
Old identities are not remapped.

  was:
h2. Motivation

Some Connector configuration properties describe decisions that cannot be 
safely changed after the Connector has started producing data. Typical examples:
 * The output format written to a destination. Once objects have been written 
to an S3 bucket as Parquet, silently switching the property to Avro produces a 
bucket whose contents can no longer be read by a single reader.
 * The physical destination identifier. The S3 bucket, Kafka topic, or 
object-store prefix that the Connector writes into. Changing this after data 
has been written silently forks the destination and leaves the previously 
written data orphaned.
 * The identity used to track source progress. The Kafka consumer group id or 
source offset-tracking key. Changing this after commit either re-reads 
already-processed data or leaves committed offsets behind, in both cases 
producing duplicates or gaps.

Today there is no first-class way for a Connector author to declare that such a 
property is frozen after the Connector has started. The intent lives only in ad 
hoc validation code inside each Connector, is not exposed to the REST API, is 
not visible in the extension manifest, and is not visible in the UI. Users can 
freely edit these fields in the configuration dialog and see a generic error 
only when they attempt to apply the change, if the Connector happens to defend 
it at all.

This proposal makes _"once the Connector has been started at least once, this 
property is frozen at its current value"_ a first-class descriptor attribute, 
enforced by the framework, surfaced through the REST API and extension 
manifest, and rendered as read-only in the UI so the user never has the 
opportunity to make an unappliable change in the first place.
h2. Scope

Out of Scope:

Immutable Properties for Processors or Controller Services are not part of the 
scope of this NIP.    Processors or Controller Services have done without this 
feature and have implemented ways to handle property value changes within the 
component code.  Connectors integrate an entire flow worth of components 
together so trying to force connectors to handle any possible connector 
property change may not always be possible.  

{{nifi-api}} additions:
 * {{ConnectorPropertyDescriptor.isImmutable()}} and 
{{{}ConnectorPropertyDescriptor.Builder.immutable(boolean){}}}, defaulting to 
{{{}false{}}}. {{Builder.from(other)}} copies the flag.
 * {{VersionedConnector.hasBeenStarted}} ({{{}boolean{}}}) with getter/setter, 
serialized alongside the Connector's other flow-level metadata.
 * {{XmlConnectorDocumentationWriter.writeConnectorProperty(...)}} emits a new 
{{<immutable>}} element so the extension manifest exposes the flag.

Framework additions:
 * New methods on {{{}org.apache.nifi.components.connector.ConnectorNode{}}}: 
{{boolean hasBeenStarted()}} and {{{}void setHasBeenStarted(boolean){}}}.
 * {{StandardConnectorNode}} sets {{hasBeenStarted = true}} after every 
successful transition to {{{}RUNNING{}}}, and rejects changes to any immutable 
property in {{setConfiguration(...)}} and {{replaceWorkingConfiguration(...)}} 
once the flag is set. Comparison is done on the raw {{ConnectorValueReference}} 
instances so asset and secret references are compared by identity, not by their 
resolved values.
 * {{VersionedComponentFlowMapper.mapConnector(...)}} persists the flag; 
{{StandardConnectorRepository.createConnectorNode(VersionedConnector)}} 
restores it via {{{}ConnectorNode.setHasBeenStarted(...){}}}. The flag survives 
restarts, flow imports, and Connector-version upgrades.

Model / DTO / manifest plumbing:
 * {{ConnectorPropertyDescriptorDTO}} (nifi-client-dto) gains an {{immutable}} 
field, populated by 
{{{}DtoFactory.createConnectorPropertyDescriptorDto(...){}}}.
 * {{ConnectorDTO}} (nifi-client-dto) gains a {{hasBeenStarted}} field, 
populated by {{DtoFactory}} from {{{}ConnectorNode.hasBeenStarted(){}}}, so 
that any client can determine whether a Connector's immutable properties are 
currently locked.
 * {{org.apache.nifi.extension.manifest.ConnectorProperty}} and 
{{org.apache.nifi.c2.protocol.component.api.ConnectorPropertyDescriptor}} gain 
the same {{immutable}} field; 
{{StandardRuntimeManifestBuilder.getConnectorPropertyDescriptor(...)}} copies 
it through.

UI additions ({{{}nifi-frontend{}}}):
 * The Connector property-editor components consume {{descriptor.immutable}} 
together with {{{}connector.hasBeenStarted{}}}. When both are true, the 
corresponding form control is rendered as disabled and no editable value 
control is offered.
 * A locked visual indicator (padlock icon plus tooltip text) is displayed next 
to the disabled control, explaining that the property was fixed at the value 
from the Connector's first successful start and cannot be changed..

h2. Description

Descriptor contract. 

A property built with {{.immutable(true)}} behaves identically to a 
non-immutable property until the Connector transitions to {{RUNNING}} for the 
first time. Attempts to change it before that point succeed, which preserves 
normal initial configuration, verification, and staged flow deployment. 
Immutability begins the moment {{StandardConnectorNode}} sets 
{{{}hasBeenStarted = true{}}}, and from then on the value locked in is whatever 
was in the active configuration at the point of transition.

Enforcement point. 

The check lives in a single private helper — 
{{StandardConnectorNode.verifyImmutablePropertiesUnchanged(stepName, incoming, 
replace)}} — called from the two mutation entry points a user request can reach:
 * {{setConfiguration(String, StepConfiguration)}} — the merge path used by 
REST configuration updates and by {{{}ConnectorTestRunner.configure(...){}}}.
 * {{replaceWorkingConfiguration(String, StepConfiguration)}} — the whole-step 
replacement path used by configuration providers.

Restore paths (flow deserialization, snapshot inheritance, 
{{{}recreateWorkingFlowContext{}}}) deliberately bypass the check because they 
must be able to reconstruct the Connector's existing state without the 
framework second-guessing itself. A violating change throws 
{{FlowUpdateException}} with a message that names the offending property, the 
step, and the Connector.

Persistence. 

{{hasBeenStarted}} is stored on {{{}VersionedConnector{}}}, not on the 
descriptor. On save, {{VersionedComponentFlowMapper}} reads 
{{{}connectorNode.hasBeenStarted(){}}}. On load, 
{{StandardConnectorRepository}} reads the field off the {{VersionedConnector}} 
and calls {{setHasBeenStarted(true)}} before the Connector is exposed to any 
user request, so enforcement is active immediately after restart. Because the 
flag lives on the Connector and not on the descriptor, values locked under 
Connector v1 remain locked under Connector v2 even if v2 changes the descriptor 
set: whether a property is immutable is decided by the current v2 descriptors, 
and _"the Connector has been started"_ is decided by the persisted flag.

Backwards compatibility.

 {{isImmutable()}} defaults to {{{}false{}}}, so existing Connectors are 
unaffected. {{VersionedConnector.hasBeenStarted}} defaults to {{false}} in the 
JSON representation, so flows saved by older frameworks come back unlocked, 
which is the same behaviour as today. The new DTO / manifest / c2 fields are 
additive, and building an older Connector against the new {{nifi-api}} 
continues to compile because both new descriptor methods have default 
implementations.


> Lockable Connector Properties
> -----------------------------
>
>                 Key: NIP-36
>                 URL: https://issues.apache.org/jira/browse/NIP-36
>             Project: NiFi Improvement Proposal
>          Issue Type: Improvement
>            Reporter: Bob Paulin
>            Priority: Major
>         Attachments: image-2026-10-16-07-49-01-251.png
>
>
> h2. Motivation
> Some Connector configuration properties describe decisions that cannot be 
> safely changed after the Connector has started producing data. Typical 
> examples:
>  * The output format written to a destination. Once objects have been written 
> to an S3 bucket as Parquet, silently switching the property to Avro produces 
> a bucket whose contents can no longer be read by a single reader.
>  * The physical destination identifier. The S3 bucket, Kafka topic, or 
> object-store prefix that the Connector writes into. Changing this after data 
> has been written silently forks the destination and leaves the previously 
> written data orphaned.
>  * The identity used to track source progress. The Kafka consumer group id or 
> source offset-tracking key. Changing this after commit either re-reads 
> already-processed data or leaves committed offsets behind, in both cases 
> producing duplicates or gaps.
> Today there is no first-class way for a Connector author to protect such a 
> property. The intent lives only in ad hoc validation code inside each 
> Connector, is not exposed to the REST API, is not visible in the extension 
> manifest, and is not visible in the UI. Users can freely edit these fields in 
> the configuration dialog and see a generic error only when they attempt to 
> apply the change, if the Connector happens to defend it at all.
> These decisions are not permanent, though. A user may deliberately move to a 
> new bucket, switch formats, or replay a topic from the beginning. Today the 
> only safe way to do that is to delete the Connector and create a new one, 
> losing the rest of its configuration and giving the Connector no chance to 
> clean up what the old value left behind.
> h2. Scope
> Out of scope:
> Lockable properties for Processors or Controller Services are not part of the 
> scope of this NIP. Processors and Controller Services have done without this 
> feature and have implemented ways to handle property value changes within the 
> component code. Connectors integrate an entire flow worth of components 
> together, so forcing Connectors to handle any possible Connector property 
> change may not always be possible.
> {{nifi-api}} additions:
>  * {{ConnectorPropertyDescriptor.isLockable()}} and 
> {{{}ConnectorPropertyDescriptor.Builder.lockable(boolean){}}}, defaulting to 
> {{{}false{}}}. {{Builder.from(other)}} copies the flag.
>  * {{VersionedConnector.lockedProperties}} with getter and setter.
>  * {{UnlockContext}} and {{{}Connector.prepareForUnlock(UnlockContext){}}}, 
> with a default implementation.
>  * {{XmlConnectorDocumentationWriter.writeConnectorProperty(...)}} emits a 
> new {{<lockable>}} element.
> Framework additions:
>  * {{ConnectorNode}} methods to read and restore the locked set, and 
> {{{}unlock(...){}}}.
>  * {{StandardConnectorNode}} records locks, enforces them, and advertises an 
> {{UNLOCK}} action through {{{}getAvailableActions(){}}}.
>  * {{VersionedComponentFlowMapper.mapConnector(...)}} persists the locked 
> set, and 
> {{StandardConnectorRepository.createConnectorNode(VersionedConnector)}} 
> restores it.
> Model, DTO, manifest, and C2 plumbing:
>  * {{ConnectorPropertyDescriptorDTO}} ({{{}nifi-client-dto{}}}) gains 
> {{{}lockable{}}}, populated by 
> {{{}DtoFactory.createConnectorPropertyDescriptorDto(...){}}}.
>  * {{ConnectorDTO}} ({{{}nifi-client-dto{}}}) gains {{{}lockedProperties{}}}, 
> populated by {{{}DtoFactory{}}}.
>  * {{org.apache.nifi.extension.manifest.ConnectorProperty}} and 
> {{org.apache.nifi.c2.protocol.component.api.ConnectorPropertyDescriptor}} 
> gain {{{}lockable{}}}. 
> {{StandardRuntimeManifestBuilder.getConnectorPropertyDescriptor(...)}} copies 
> it through.
>  * The Connector REST resource gains an unlock endpoint, following the 
> existing Connector action endpoints.
> UI additions ({{{}nifi-frontend{}}}):
>  * The Connector property editor shows lock status, and offers unlock and 
> reset controls.
> h2. Description
> h3. Descriptor contract
> {{lockable}} is a static, author-declared strategy on the shared 
> {{{}ConnectorPropertyDescriptor{}}}. It does not describe the state of any 
> Connector instance. A lockable property behaves like any other property until 
> the Connector successfully transitions to {{{}RUNNING{}}}, so initial 
> configuration, verification, and staged flow deployment are unaffected.
> h3. Lock status
> Each Connector instance keeps a persisted set of locked property identities, 
> exposed as {{{}lockedProperties{}}}. An identity is a configuration step name 
> plus a property name; {{ConfigurationStep.Builder}} already requires property 
> names to be unique within a step.
> A property is LOCKED when its identity is in the set and its current 
> descriptor is {{{}lockable{}}}. Otherwise it is UNLOCKED.
> On every successful transition to {{{}RUNNING{}}}, the framework adds every 
> property whose current descriptor is {{{}lockable{}}}, locking the value in 
> the active configuration at that moment. No other state transition adds or 
> removes locks, including entering or leaving {{{}TROUBLESHOOTING{}}}. Unlock 
> is the only operation that removes identities.
> h3. Enforcement
> A single private helper, 
> {{{}StandardConnectorNode.verifyLockedPropertiesUnchanged(...){}}}, is called 
> from the two mutation entry points a user request can reach:
>  * {{{}setConfiguration(String, StepConfiguration){}}}: the merge path used 
> by REST configuration updates and by 
> {{{}ConnectorTestRunner.configure(...){}}}.
>  * {{{}replaceWorkingConfiguration(...){}}}: the whole-configuration 
> replacement path used by configuration providers.
> For each LOCKED property, the helper compares the incoming raw 
> {{ConnectorValueReference}} with the active one, so asset and secret 
> references are compared by identity, not by resolved value. An equal 
> reference is accepted. A different reference throws {{FlowUpdateException}} 
> naming the property, the step, and the Connector.
> Restore paths (flow deserialization, snapshot inheritance, 
> {{{}recreateWorkingFlowContext{}}}) bypass the check because they reconstruct 
> existing state.
> !image-2026-10-16-07-49-01-251.png!
> h3. Unlock and reset
> {{UNLOCK}} is a framework Connector action alongside existing actions such as 
> {{START}} and {{{}PURGE_FLOWFILES{}}}. It is allowed when the Connector is 
> stopped, is not in Troubleshooting, and has at least one LOCKED property. 
> Otherwise it is listed as disallowed with a reason.
> The request carries an optional set of property identities:
>  * No identities resets the Connector: every LOCKED property is unlocked, and 
> {{UnlockContext.isReset()}} is true.
>  * A non-empty set unlocks only those identities. If any identity is not 
> currently LOCKED, the request fails and nothing is removed.
> Before changing the set, the framework calls 
> {{{}Connector.prepareForUnlock(UnlockContext){}}}. The locked values and the 
> active flow are still in place. {{UnlockContext}} provides the identities, 
> the active {{ConnectorValueReference}} for each, the active flow context, and 
> {{{}isReset(){}}}.
> The Connector uses this hook to drop committed offsets, abandon a 
> destination, or refuse the change. It refuses synchronously by throwing 
> {{{}FlowUpdateException{}}}, or asynchronously by completing the returned 
> {{CompletableFuture}} exceptionally. Either keeps every lock. The framework 
> cancels the future if the caller cancels the request, so delegated work must 
> be cooperatively cancellable, and the future must not complete while other 
> work can still mutate the flow. The default implementation returns a 
> completed future.
> When the future completes normally, the framework removes the identities and 
> persists the Connector. Active values do not change. The user then edits the 
> properties through the normal configuration path, so validation, 
> {{{}onConfigurationStepConfigured{}}}, {{{}prepareForUpdate{}}}, and 
> {{applyUpdate}} all still apply.
>  
> The next successful start locks them again.
>  
> h3. Persistence and upgrades
> {{VersionedComponentFlowMapper}} writes the locked set to 
> {{{}VersionedConnector{}}}. {{StandardConnectorRepository}} restores it 
> before the Connector accepts user requests, so enforcement is active 
> immediately after restart. The set, including the result of a partial unlock, 
> survives restarts, flow imports, and Connector version upgrades.
> Because locks are tracked per property:
>  * A property that becomes lockable only in a later Connector version stays 
> editable until the next successful start.
>  * An identity whose current descriptor is not lockable is ignored. Removing 
> {{lockable}} in a later version is how an author retires the constraint.
>  * A renamed property is a new identity and locks on the next successful 
> start. Old identities are not remapped.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to