[#1120] Apply a change to plugin-type on an enabled plugin without disabling and enabling it again - #1127
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: The fix sits where the bug is and fails safe.
replacePlugininitializes the new instance before it touches the running one, so a refusal ininitializePlugin()leaves the plugin working (PluginConfigManager.java:4553-4562).component-restartis dropped fromplugin-typeinPluginConfiguration.xmlin the same change, sodsconfighelp stops contradicting the code.PluginTypeTrackingPluginrefuses its types only ininitializePlugin(), which is exactly the case the acceptance phase cannot catch.
suggestion (non-blocking): Register the new instance before finalizing the old one, so the plugin is never missing for the length of finalizePlugin().
opendj-server-legacy/src/main/java/org/opends/server/core/PluginConfigManager.java:4567-4568
deregisterPlugin(dn) removes the old instance from every type array, the unchanged types included, and then calls finalizePlugin(). registerPlugin runs only after that. The invoke* methods read the arrays without pluginLock, so for the whole of finalizePlugin() the plugin runs for no type. For ReferentialIntegrityPlugin with update-interval > 0, finalizePlugin → processServerShutdown joins the background thread, and the join waits for a running processLog to finish. A delete arriving in that time is never logged, so its references are never cleaned, and check-references is skipped. The description calls this "the gap between removing the old instance and adding the new one, per plugin type". In fact it covers all types and lasts as long as the finalize. Finalizing after registering keeps the "never invoked twice" property, because the old instance is already out of the arrays, and it shrinks the gap to the array writes.
pluginLock.lock();
try
{
DirectoryServerPlugin<? extends PluginCfg> oldPlugin = registeredPlugins.remove(configuration.dn());
if (oldPlugin != null)
{
deregisterPlugin0(oldPlugin);
}
registerPlugin(plugin, configuration.dn(), pluginTypes);
if (oldPlugin != null)
{
oldPlugin.finalizePlugin();
}
}
finally
{
pluginLock.unlock();
}RI's writeLog/processLog lock each instance's own File (ReferentialIntegrityPlugin.java:789, :813, :840). So after this change a delete logged by the new instance while the old thread is still in processLog can still be lost. That is no worse than today, where the delete is not logged at all.
issue (non-blocking): If the old instance's finalizePlugin() throws, the plugin ends up registered in neither form.
opendj-server-legacy/src/main/java/org/opends/server/core/PluginConfigManager.java:4567
deregisterPlugin calls finalizePlugin() without a catch, and the try/finally in replacePlugin only unlocks. A RuntimeException there skips registerPlugin. The entry is then gone from registeredPlugins and from every array. The new instance, with its listeners and threads already started, is never registered, and the modify fails with the unchecked exception. No in-tree finalizePlugin throws, so only a faulty third-party plugin gets here. The reorder above fixes it: with registerPlugin before finalizePlugin(), a throwing finalize no longer removes the plugin.
question (non-blocking): After a refused change, should an unrelated later modify of the entry still fail?
opendj-server-legacy/src/main/java/org/opends/server/core/PluginConfigManager.java:4499-4503
replaceEntry stores the refused plugin-type before the listeners run. After that, every later change compares the stored types with the running instance's types, retries replacePlugin, and returns before setInvokeForInternalOperations. So after the refused modify in testPluginTypeChangeRefusedByPluginKeepsRunningPlugin, replace: ds-cfg-invoke-for-internal-operations: false fails with the plugin-type error. It is stored anyway, and the running plugin keeps running for internal operations. At BASE the same modify succeeded. The description says the stored and running types differ "until the next change", but for an unrelated next change they still differ, and that change fails too. Minor either way. If it is intended, a sentence in the description or the dev guide covers it. If not, the unrelated properties could still be applied to the running instance on the refusal road.
issue (non-blocking): On the refusal road, whatever the rejected instance registered before initializePlugin() threw stays registered next to the running instance.
opendj-server-legacy/src/main/java/org/opends/server/core/PluginConfigManager.java:4557-4562, opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java:169, :180
ReferentialIntegrityPlugin.initializePlugin adds its change listener first. It throws only later, in setUpLogFile, when the log file cannot be created. isConfigurationAcceptable never reads log-file. So a modify that changes plugin-type and sets log-file under a missing directory passes acceptance and then fails in apply. The rejected instance then stays registered as a listener on the same DN, and every later change of the entry retries and adds one more. loadPlugin has never finalized a failed instance: the add and enable roads do the same at BASE, but there no live instance sits beside it. A guarded finalizePlugin() on the failure path in loadPlugin would fix all three roads. A follow-up is fine.
issue (non-blocking): SambaPasswordPlugin and PasswordPolicyImportPlugin never remove their change listener, so each replace leaves a finalized instance listening on the entry.
opendj-server-legacy/src/main/java/org/opends/server/plugins/SambaPasswordPlugin.java:862, opendj-server-legacy/src/main/java/org/opends/server/plugins/PasswordPolicyImportPlugin.java:109
Both plugins add their listener in initializePlugin and do not override finalizePlugin, whose default does nothing. The description says "the old instance removes its listener in finalizePlugin(), so it is not called for the change after it is finalized". That is not true for these two. The old listener is still registered, so it is in replaceEntry's snapshot, and the finalized instance receives this change and every later one. Each plugin-type change adds one more. This is harmless today and the same leak already happens on disable/enable. You could fix it here with a finalizePlugin that removes the listener, or narrow the sentence in the description.
issue (non-blocking): The indexed invokePreOperation*Plugins loops read the array field again on every iteration, so a swap in the middle of an operation can skip an unrelated plugin or throw ArrayIndexOutOfBoundsException.
opendj-server-legacy/src/main/java/org/opends/server/core/PluginConfigManager.java:2325-2358 (and the other seven pre-operation loops)
The field is plain and is read in the loop condition, in the element access, and again when it is passed to handlePreOperationException / registerSkippedPreOperationPlugins, all without a lock. If X is removed from a slot before i, the next read skips the plugin that was at i. If the length comes from the old array and the element from the shorter new one, the access throws. This predates the PR: enable, disable, add and delete already reach it. The PR adds a plugin-type change as one more trigger. A follow-up is fine.
DirectoryServerPlugin[] plugins = preOperationAddPlugins;
for (int i = 0; i < plugins.length; i++)
{
DirectoryServerPlugin p = plugins[i];
// ... and pass plugins, not the field, to handlePreOperationException,
// handlePreOperationResult and registerSkippedPreOperationPluginssuggestion (non-blocking): The countChangeListeners(pluginDN) asserts cannot fail on a plugin-instance listener fault, because the test plugin registers no listener.
opendj-server-legacy/src/test/java/org/opends/server/core/PluginConfigManagerTestCase.java:673, :680
PluginTypeTrackingPlugin adds no change listener, so the only listener on the entry is the manager's, and replacePlugin never touches it. If an old instance kept its listener, or replaceEntry called a finalized instance, both asserts would still compare 1 with 1. The description cites these asserts for "the number of change listeners on the entry does not change". The finalizePlugin() mutant is killed, but by isFinalized().
private volatile PluginCfg configuration;
private static final AtomicInteger CHANGES_SEEN_WHEN_FINALIZED = new AtomicInteger();
private final ConfigurationChangeListener<PluginCfg> listener = new ConfigurationChangeListener<PluginCfg>()
{
@Override
public boolean isConfigurationChangeAcceptable(PluginCfg cfg, List<LocalizableMessage> reasons)
{
return true;
}
@Override
public ConfigChangeResult applyConfigurationChange(PluginCfg cfg)
{
if (finalized)
{
CHANGES_SEEN_WHEN_FINALIZED.incrementAndGet();
}
return new ConfigChangeResult();
}
};
// initializePlugin(): this.configuration = configuration; configuration.addChangeListener(listener);
// finalizePlugin(): configuration.removeChangeListener(listener); finalized = true;Pin: with the listener in place, the unchanged count covers the plugin's own listener, and CHANGES_SEEN_WHEN_FINALIZED == 0 after each replace pins the snapshot skip the description relies on.
suggestion (non-blocking): No test covers the rule that a java-class change in the same modify leaves the plugin types unapplied.
opendj-server-legacy/src/main/java/org/opends/server/core/PluginConfigManager.java:4497
If the else is dropped, a modify that changes java-class and plugin-type together calls replacePlugin. That loads the new class at once, which contradicts "a change of the Java class keeps the current behaviour". The mutant survives: the new cases change plugin-type alone, and no test modifies ds-cfg-java-class on an existing entry.
ModifyRequest request = newModifyRequest(pluginDN)
.addModification(REPLACE, "ds-cfg-java-class", PluginTypeTrackingPlugin2.class.getName())
.addModification(REPLACE, "ds-cfg-plugin-type", "postOperationModify", "preOperationModify");
getRootConnection().processModify(request);
assertSame(getTrackingPlugin(pluginDN), original);
assertFalse(original.isFinalized());
assertEquals(original.getPluginTypes(), EnumSet.of(POST_OPERATION_MODIFY));Pin: PluginTypeTrackingPlugin2 is a trivial top-level subclass of PluginTypeTrackingPlugin. assertSame fails against the mutant.
suggestion (non-blocking): The replacement's post-operation registration is never observed.
opendj-server-legacy/src/test/java/org/opends/server/plugins/PluginTypeTrackingPlugin.java:78
doPostOperation records nothing, and getPluginTypes() returns the set handed to initializeInternal, not what the manager registered. Take a replacePlugin that registers only the added types. On {POST}→{POST,PRE} it registers only PRE. On {POST,PRE}→{POST} it registers nothing, so the plugin runs for no type. Both cases, the refused case and the RI case all stay green against it.
private static final ConcurrentHashMap<DN, AtomicInteger> POST_OPERATION_MODIFY_COUNTS = new ConcurrentHashMap<>();
@Override
public PluginResult.PostOperation doPostOperation(PostOperationModifyOperation modifyOperation)
{
POST_OPERATION_MODIFY_COUNTS.computeIfAbsent(modifyOperation.getEntryDN(), dn -> new AtomicInteger())
.incrementAndGet();
return PluginResult.PostOperation.continueOperationProcessing();
}Pin: add a takePostOperationModifyCount and assert 1 in modifyTestEntryAndCountPreOperation. Against the mutant, the post-operation count after the first change is 0.
…nalizing the old one, and finalize a plugin that fails to initialize Review round 1 of OpenIdentityPlatform#1127: - replacePlugin() registers the new instance in place of the old one and only then finalizes the old one, outside pluginLock, and an exception from that finalizePlugin() is logged instead of leaving the plugin unregistered. - loadPlugin() finalizes a plugin whose initializePlugin() fails, so a change listener it registered first does not stay behind. This covers startup, add, enable and the replacement. - invoke-for-internal-operations is applied to the running instance also when the plugin refuses the new plugin types. - The pre-operation invoke methods read the plugin array once, so a concurrent registration change cannot skip a plugin or make the index run past the end of the array. - SambaPasswordPlugin and PasswordPolicyImportPlugin remove their change listener in finalizePlugin(). - Tests: the test plugin registers a change listener and counts post-operation modifies; new cases for a failing finalizePlugin() and for a Java class change together with the plugin types; listener counts for the Samba and password policy import plugins.
ce6ff67 to
aafa02d
Compare
|
All nine points are addressed in aafa02d. The branch is also rebased onto the current master (#1122, #1124). There were no conflicts, and the first commit is unchanged. 1. Register before finalize. Done, as in your snippet, with one difference: the old instance is finalized after 2. Throwing 3. Unrelated change after a refusal. Not intended. 4. Leftovers of the rejected instance. Fixed here, not as a follow-up, because of (3): every later change retries, so a leaked instance would pile up once per change. The RI zombie is also not quite harmless. It throws in 5. Samba / PasswordPolicyImport listeners. Both now remove their listener in 6. Indexed pre-operation loops. Fixed here as well: the eight methods read the array into a local once and pass that local on. There is no test, because the race needs a registration change between two reads inside one invocation. Side note, left as it is: 7. Listener in the test plugin. Done along the lines of your snippet: the listener counts changes seen after 8. 9. Post-operation registration. Local runs: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The second commit closes every first-round point where the bug is, and pins most of them.
replacePluginregisters the new instance before it finalizes the old one, outsidepluginLockand throughfinalizePluginQuietly; the test plugin recordsgetRegisteredPlugin(dn)insidefinalizePlugin(), sogetRegisteredWhenFinalized()pins the order.- The eight
invokePreOperation*Pluginsmethods read the array once and pass that same local tohandlePreOperation*andregisterSkippedPreOperationPlugins. setInvokeForInternalOperationsnow runs before the class and type checks, pinned byassertFalse(running.invokeForInternalOperations())in the refused case.
issue (non-blocking): The finalize-on-failed-init in loadPlugin does not remove the change listener of ReferentialIntegrityPlugin when RI's own configuration check fails at startup.
opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java:169, :174, :889, opendj-server-legacy/src/main/java/org/opends/server/core/PluginConfigManager.java:408
initializePlugin adds its listener at :169 and throws at :174 when isConfigurationAcceptable fails. currentConfiguration is assigned only at :250, inside applyConfigurationChange, which runs after that throw. So finalizePlugin() throws an NPE at :889 before it removes anything, finalizePluginQuietly only traces the NPE, and the listener stays. Only startup reaches this, because initializeUserPlugins has no acceptance phase; add, enable and replace run the same check first. Example: an RI attribute-type whose equality index was removed before a restart. The plugin is logged and skipped, but the skipped instance keeps listening, and with update-interval > 0 a later change of the entry makes it start a background thread. The leak predates the PR. The commit message and the description say the finalize "covers startup, add, enable and the replacement", and the new dev-guide paragraph asks plugin authors for exactly what RI does not do. The setUpLogFile failure is covered: currentConfiguration and interval are set by then.
LinkedList<LocalizableMessage> unacceptableReasons = new LinkedList<>();
if (!isConfigurationAcceptable(pluginCfg, unacceptableReasons))
{
throw new ConfigException(unacceptableReasons.getFirst());
}
// Only after the check, so that a refused configuration leaves no listener behind.
pluginCfg.addReferentialIntegrityChangeListener(this);
applyConfigurationChange(pluginCfg);Or, in addition: guard finalizePlugin() with if (currentConfiguration != null). That way the refused road does not throw an NPE that is only traced.
issue (non-blocking): MsadPlugin never removes its change listener, so every plugin-type change and every refused retry leaves a stale listener.
opendj-server-msad-plugin/src/main/java/opendj/MsadPlugin.java:68, :86, :189-192
The plugin adds config.addMsadChangeListener(this) before its type switch throws for anything outside bind, add and modify. It overrides neither finalizePlugin() nor isConfigurationAcceptable(). When the types change from {preOperationBind} to {preOperationBind, preOperationAdd}, replacePlugin finalizes the old instance with the no-op default, and its listener stays. A refused type, such as postOperationAdd, passes the acceptance phase and leaks the refused instance's listener, and every later change of the entry repeats that. From then on each stale instance logs "changed MSAD plugin configuration" on every change and stays in memory until restart. The PR fixes this for SambaPasswordPlugin and PasswordPolicyImportPlugin. This one is in another module, and my first round missed it. The removal matches by config DN, so the cfg object that applyConfigurationChange replaced still works.
@Override
public void finalizePlugin() {
config.removeMsadChangeListener(this);
}suggestion (non-blocking): No case pins that a change which leaves the plugin types alone keeps the running instance.
opendj-server-legacy/src/main/java/org/opends/server/core/PluginConfigManager.java:4558, opendj-server-legacy/src/test/java/org/opends/server/core/PluginConfigManagerTestCase.java:678
Suppose the guard !pluginTypes.equals(existingPlugin.getPluginTypes()) is dropped. Then every change of an enabled plugin's entry re-creates the plugin and drops its in-memory state. Every modify in the new cases already runs replacePlugin with or without the guard: it changes the types, it runs while refused types are stored (the invoke-for-internal-operations modify at :724), or it changes the class. No other case I traced holds an instance across a same-type config modify. This was traced, not run. The RI, SevenBitClean, UniqueAttribute and AttributeCleanup suites were not traced.
// A change that leaves the plugin types alone keeps the running instance.
assertEquals(getRootConnection().processModify(newModifyRequest(pluginDN)
.addModification(REPLACE, "ds-cfg-invoke-for-internal-operations", "false")).getResultCode(),
ResultCode.SUCCESS);
assertSame(getTrackingPlugin(pluginDN), replacement);
assertFalse(replacement.isFinalized());
assertEquals(getRootConnection().processModify(newModifyRequest(pluginDN)
.addModification(REPLACE, "ds-cfg-invoke-for-internal-operations", "true")).getResultCode(),
ResultCode.SUCCESS);Pin: in testPluginTypeChangeAppliesToEnabledPlugin after :678. With the guard dropped, the first modify re-creates the plugin and assertSame fails.
nitpick (non-blocking): The DirectoryServerPlugin.finalizePlugin() Javadoc still says it runs only after the plugin is deregistered.
opendj-server-legacy/src/main/java/org/opends/server/api/plugin/DirectoryServerPlugin.java:167-170
loadPlugin now also calls it on an instance whose initializePlugin() threw and that was never registered. Only chap-writing-plugins.adoc states the new duty, and the RI issue above is an in-tree plugin that does not meet it.
/**
* Performs any necessary finalization for this plugin. This will
* be called just after the plugin has been deregistered with the
* server but before it has been unloaded. It is also called when
* {@link #initializePlugin} throws, on an instance that was never
* registered: it must then release whatever {@code initializePlugin}
* acquired before it failed, and must not assume that it completed.
*/…ed plugin without disabling and enabling it again When plugin-type changes on an enabled plugin, the plugin config manager now initializes a new instance of the plugin for the new plugin types and then replaces the registered instance with it. If the plugin refuses the new types in initializePlugin(), the change fails and the running instance stays registered for its old types. plugin-type no longer declares component-restart, and the plugin developer guide describes how a running plugin is re-created.
…nalizing the old one, and finalize a plugin that fails to initialize Review round 1 of OpenIdentityPlatform#1127: - replacePlugin() registers the new instance in place of the old one and only then finalizes the old one, outside pluginLock, and an exception from that finalizePlugin() is logged instead of leaving the plugin unregistered. - loadPlugin() finalizes a plugin whose initializePlugin() fails, so a change listener it registered first does not stay behind. This covers startup, add, enable and the replacement. - invoke-for-internal-operations is applied to the running instance also when the plugin refuses the new plugin types. - The pre-operation invoke methods read the plugin array once, so a concurrent registration change cannot skip a plugin or make the index run past the end of the array. - SambaPasswordPlugin and PasswordPolicyImportPlugin remove their change listener in finalizePlugin(). - Tests: the test plugin registers a change listener and counts post-operation modifies; new cases for a failing finalizePlugin() and for a Java class change together with the plugin types; listener counts for the Samba and password policy import plugins.
…tial integrity plugin refused at startup, and remove the MSAD plugin's listener on finalize Review round 2 of OpenIdentityPlatform#1127: - ReferentialIntegrityPlugin registers its change listener only after its configuration check, and finalizePlugin() returns early when the check refused the configuration. Before, the listener was registered first and finalizePlugin() failed with a NullPointerException before it removed it, so an instance refused at startup, where there is no acceptance phase, kept listening to its entry. - MsadPlugin removes its change listener in finalizePlugin(), so neither a plugin type change nor a refused plugin type leaves a listener behind. SambaPasswordPlugin.finalizePlugin() no longer fails on an instance that refused its configuration. - The DirectoryServerPlugin.finalizePlugin() Javadoc says that it is also called on an instance whose initializePlugin() threw. - OpenIdentityPlatform#1123 told the administrator to disable and re-enable the referential integrity plugin after adding plugin types, in chap-groups.adoc and in the check-references error and warning. Added plugin types now take effect at once, so the sentence is replaced and the advice dropped from both messages. - Tests: a change that leaves the plugin types alone keeps the running instance; a referential integrity configuration refused by initializePlugin() leaves no change listener behind.
aafa02d to
d4b0749
Compare
|
All four points are addressed in d4b0749. The branch is also rebased onto the current master, which now has #1123. There were no conflicts, and only the base of the first two commits changed. 1. RI listener on a configuration refused at startup. Done as in your snippet, plus the guard. 2. 3. Same plugin types keep the instance. Your snippet is in 4. Javadoc. Your text is in Also, after the rebase onto #1123. As the description said, the PR merged second has to remove the "disable and re-enable" sentence. It is replaced in Local runs, reactor with #1123: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The new commit closes every round-2 point the way it was offered, and pins the main one.
ReferentialIntegrityPluginregisters its change listener only after the configuration check (ReferentialIntegrityPlugin.java:177-182),finalizePlugin()returns early on a refused configuration (:934-938), andtestConfigurationRefusedByInitializePluginLeavesNoChangeListenerBehindcounts the entry's listeners on both refusal roads (the check andsetUpLogFile).MsadPlugin.finalizePlugin()(MsadPlugin.java:109-113) removes the last in-tree plugin listener that leaked on finalize, and theDirectoryServerPlugin.finalizePlugin()Javadoc (:171-175) now states the failed-init call.- The #1123 texts that told the operator to disable and re-enable the plugin after adding plugin types are updated (
plugin.properties:357-365,chap-groups.adoc:542), andPluginConfigManagerTestCase:680-686pins that a modify leaving the plugin types alone keeps the running instance.
Fixes #1120
Problem
When
plugin-typechanged on an enabled plugin,PluginConfigManager.applyConfigurationChangecompared only the Java class, updatedinvoke-for-internal-operationsand returned. The plugin stayed registered for the plugin types it was enabled with, the change was stored, and nothing was reported. The new types took effect only after the plugin was disabled and enabled again or the server was restarted. This is how #579 ended up withcheck-references:truethat checked nothing.Change
PluginConfigManager.applyConfigurationChange: when the class is unchanged and the plugin types differ fromexistingPlugin.getPluginTypes(), the newreplacePlugin()runs:loadPlugin(..., true), the same call the "disabled → enabled" branch uses);pluginLock, it takes the running instance out of the plugin arrays and registers the new one in its place. Only after that, outside the lock, does it callfinalizePlugin()of the old instance. An exception from that call is logged, as infinalizePlugins().A change of the Java class keeps the current behaviour (
adminActionRequired).invoke-for-internal-operationsis now applied to the running instance before the plugin types are compared, so it also takes effect when the new types are refused.loadPlugin(): wheninitializePlugin()throws, the failed instance is finalized (quietly). A plugin that registered a change listener or started a thread before it failed no longer leaves it behind. This covers startup, add, enable and the replacement.The eight
invokePreOperation*Pluginsmethods read the plugin array into a local once. Before, they read the field again for the loop bound, the element and the array passed tohandlePreOperation*/registerSkippedPreOperationPlugins, so a concurrent registration change could skip a plugin or throwArrayIndexOutOfBoundsException. This predates the PR, but aplugin-typechange is one more way to trigger it.SambaPasswordPlugin,PasswordPolicyImportPluginandMsadPluginnow remove their change listener infinalizePlugin(). Before, a finalized instance kept listening to the entry after every disable/enable, and for Samba and MSAD also after every plugin type change. For MSAD, a refused plugin type also left the refused instance listening.ReferentialIntegrityPluginregisters its change listener only after its configuration check, andfinalizePlugin()returns early when the check refused the configuration. Before, the listener was registered first.finalizePlugin()then failed with aNullPointerExceptionbefore it removed it. So an instance refused when the server started, where there is no acceptance phase, kept listening to its entry, and a later change ofupdate-intervalcould start its background thread.The
DirectoryServerPlugin.finalizePlugin()Javadoc now says that the method is also called on an instance whoseinitializePlugin()threw.PluginConfiguration.xml:plugin-typeno longer declarescomponent-restart, sodsconfighelp and the configuration reference no longer say the change needs the component to be restarted.chap-writing-plugins.adoc: a paragraph for plugin authors on how a change toplugin-typere-creates the plugin, and on whatfinalizePlugin()must release wheninitializePlugin()fails.Why a new instance, not the running one moved to other types
A plugin receives its plugin types only in
initializeInternal()/initializePlugin().DirectoryServerPlugin.pluginTypeshas no setter, and it is public plugin API. The default hooks throwUnsupportedOperationException, and the defaultisConfigurationAcceptable()returnstrue. So a third-party plugin that checks its types only ininitializePlugin()passes the acceptance phase. Moved in place, it would then fail every operation of a type it does not support. A new instance goes throughinitializePlugin(), and a refusal is caught before anything is registered.The configuration change listeners handle the swap.
ConfigurationHandler.replaceEntryiterates a snapshot of the listeners and skips the ones that are no longer registered. The in-tree plugins remove their listener infinalizePlugin(), so the old instance is not called for the change after it is finalized. The new instance's listener is not in the snapshot, but that instance was already initialized with the new configuration. The manager's own listener comes first in the list, because it is registered before the plugin's.Trade-offs
finalizePlugin(), which for the referential integrity plugin withupdate-interval > 0waits for the background thread.UniqueAttributePluginor the background thread of the referential integrity plugin. Withupdate-interval > 0, the old and new referential integrity instances briefly hold the same log file. Each synchronizes on its ownFileobject, so a delete logged by the new instance while the old thread is still inprocessLogcan be lost. With the order of the first round, where the old instance was finalized first, such a delete was not logged at all. The new background thread first processes the file after one update interval.plugin-typediffers from what is running, and every later change of the entry retries the replacement and fails with the same reason untilplugin-typeis set to types the plugin accepts. The other properties of such a change are still applied to the running instance.Interaction with #1123
#1123 was merged first. It told the administrator to disable and re-enable the referential integrity plugin after adding plugin types. The advice was in
chap-groups.adoc("Plugin types take effect when the plugin is enabled, so add them before enabling the plugin, or disable and re-enable it afterwards.") and inERR_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE/WARN_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE. With this change, added plugin types take effect at once. So the sentence now says so, and both messages end with "Add '…' to 'plugin-type', or set 'check-references' to false".Tests
PluginConfigManagerTestCase. The test pluginPluginTypeTrackingPlugincounts pre- and post-operation modify invocations per entry, registers a change listener ininitializePlugin()before it checks its types, and removes it infinalizePlugin(). It recordsfinalizePlugin(), which plugin was registered for its entry at that moment, and whether its listener received a change after it was finalized. It refusesPRE_OPERATION_DELETEonly ininitializePlugin(), and a static switch makesfinalizePlugin()throw.testPluginTypeChangeAppliesToEnabledPlugin: addingpreOperationModifyto the enabled plugin gives it a new instance with the new types, which is invoked. The old instance is finalized after the new one is registered, the number of change listeners on the entry does not change, and no finalized instance receives a change. A change that leaves the plugin types alone keeps the running instance. Removing the type again works the same way.testPluginTypeChangeRefusedByPluginKeepsRunningPlugin: a change to a type the plugin refuses fails. The running instance stays registered, not finalized, with its types, and is still invoked. The refused instance leaves no listener behind. A later change ofinvoke-for-internal-operationsstill fails, but is applied to the running instance.testPluginTypeChangeWhenOldInstanceFailsToFinalize: when the old instance'sfinalizePlugin()throws, the change succeeds and the new instance stays registered.testPluginTypeChangeWithJavaClassChangeKeepsRunningPlugin: a modify that changesjava-class(toOtherPluginTypeTrackingPlugin) together withplugin-typeleaves the running instance and its types unchanged.ReferentialIntegrityPluginTestCase.testEnforceIntegrityAfterPluginTypesAddedToEnabledPlugin: the scenario from the issue. While the plugin stays enabled,preOperationAdd/preOperationModifyare added andcheck-referencesis set. Adding a group with a missing member must then be refused.ReferentialIntegrityPluginTestCase.testConfigurationRefusedByInitializePluginLeavesNoChangeListenerBehind: for a configuration refused by the plugin's check and for one refused when the log file is set up,initializePlugin()followed byfinalizePlugin(), asloadPlugin()does, leaves the number of change listeners on the entry unchanged and does not throw.MsadPluginhas no test: its module has no test support, and the same change inSambaPasswordPluginis pinned.SambaPasswordPluginTestCase.testPluginTypeChangeLeavesNoChangeListenerBehindandPasswordPolicyImportPluginTestCase.testDisableAndEnableLeavesNoChangeListenerBehind: the number of change listeners on the entry does not change across a plugin type change and a disable/enable.Local runs, in the reactor on top of #1123:
PluginConfigManagerTestCase38/38,ReferentialIntegrityPluginTestCase61/61 (with the [#1118] Register the shipped Referential Integrity plugin for the pre-operation types check-references needs, and refuse check-references without them #1123 cases),SambaPasswordPluginTestCase25/25,PasswordPolicyImportPluginTestCase59/59.opendj-server-msad-pluginbuilds.ReferentialIntegrityPluginregistering its listener before the configuration check, orfinalizePlugin()without thenullguard;finalizePlugin()escape;initializePlugin()failed;invoke-for-internal-operationsonly when the plugin types are unchanged;SambaPasswordPlugin/PasswordPolicyImportPluginwithoutfinalizePlugin().The snapshot of the pre-operation arrays has no test: the race needs a registration change between two reads inside one invocation.
DirectoryServerPluginTestCase50,AttributeCleanupPluginTestCase11,EntryUUIDPluginTestCase61,LDAPADListPluginTestCase58,LastModPluginTestCase61,SevenBitCleanPluginTestCase12,UniqueAttributePluginTestCase16 pass. In the first round, before the rebase,AddOperationTestCase138,ModifyOperationTestCase936,DeleteOperationTestCase195 andSearchOperationTestCase75 passed too.