[JENKINS-75232] Prevent dynamic plugin installation from registering the same extension twice in some cases#10240
Conversation
…the same extension twice in some cases Co-authored-by: Julien Greffe <jgreffe@cloudbees.com>
| @Restricted(NoExternalUse.class) | ||
| public boolean refresh(ExtensionComponentSet delta) { |
There was a problem hiding this comment.
This is an incompatible API change, but given the Javadoc, there really should not be any external callers, and a quick GitHub search (I also checked the cloudbees org) looked ok.
If desired, I can instead introduce a new method and preserve the old one for compatibility. We might still want to add @Restricted(NoExternalUse.class) to the old method if we take that approach though.
| // Refresh all extension lists before firing any listeners in case a listener would cause any new extension | ||
| // lists to be forcibly loaded, which may lead to duplicate entries for the same extension object in a list. | ||
| for (var el : listsToFireOnChangeListeners) { | ||
| el.fireOnChangeListeners(); |
There was a problem hiding this comment.
I am also happy to consider alternative fixes if anyone has any ideas. One thing I considered was to instead check for duplicates in
but delaying the listener notifications seemed clearer in terms of what we are trying to fix. In general this bug makes me think that there might be deeper problems with dynamic extension loading, but I am not familiar enough with the extension system to know how best to fix things.There was a problem hiding this comment.
Duplicate prevention added in 6817532 to fix issues with extensions that load other extensions in their constructors.
jglick
left a comment
There was a problem hiding this comment.
Source code looks fine. Recommend trying another approach for test plugin maintenance.
There was a problem hiding this comment.
Whilst this fixes the test case, I can't help but feel this doesn't fix the case where an Extension loads another extension (either in its constructor, or a Thread it kicks off).
The listener causing a load of an extension by proxy of calling some code, would just be one trigger of the wider cause here?
The suggested alternative of checking for duplicates and not using addAll would seem sane, however IIUC we would create 2 objects for the same extension (not the return the same Object). So if the extension is kicking off something async in its constructor things have already got into a bad state (and the plugin is already liable highly suspect to race conditions)?
Iff we are only creating a single extension for the class but registering it twice I think the addAll fix would be more robust
Ok, I can check this case.
As far as I can tell, right now we really are only creating one object, just registering it twice. I'll recheck though. |
if that is the case it may be worthwhile doing both.
|
@jtnord Indeed this case is still broken. I confirmed that in the problematic cases the extension is only instantiated once but registered twice. I'll add duplicate prevention to |
it's a shame that |
|
Please take a moment and address the merge conflicts of your pull request. Thanks! |
Yeah, in retrospect it seems that having this class actually be a collection was not really necessary. If we had instead only defined collection-like methods to covered the critical use cases, I would feel more comfortable refactoring the internal representation. |
jtnord
left a comment
There was a problem hiding this comment.
Seems reasonable, did not pay much attention to the test code.
jglick
left a comment
There was a problem hiding this comment.
Main code change is opaque to me, but test looks nice.
smells like a regression at least in test code? |
|
Please take a moment and address the merge conflicts of your pull request. Thanks! |
Ah, yes, |
| */ | ||
| @Issue("JENKINS-60449") | ||
| @WithPlugin("variant.hpi") | ||
| @Test public void installDependedOptionalPluginWithoutRestart() throws Exception { |
There was a problem hiding this comment.
Moved into ExtensionListRjrTest.installDependedOptionalPluginWithoutRestart.
Really, most of the tests here should probably be migrated to use RealJenkinsRule and synthetic plugins, but there are a lot of them.
| // | ||
| @Issue("JENKINS-50336") | ||
| @Test | ||
| public void optionalExtensionCanBeFoundAfterDynamicLoadOfVariant() throws Exception { |
There was a problem hiding this comment.
Also moved into ExtensionListRjrTest.installDependedOptionalPluginWithoutRestart.
I did make some changes though to the test to make it more closely match the scenario described in JENKINS-50336 (also so that I could delete variant.hpi).
| public void enableNoPluginsWithRestartIsNoOp() { | ||
| assumeNotWindows(); | ||
| String name = "variant"; | ||
| String name = "icon-shim"; |
There was a problem hiding this comment.
Just swapping for another no-op plugin that is already here. Probably all of these tests should be using RealJenkinsRule and synthetic plugins though.
| @Issue({ "JENKINS-50336", "JENKINS-60449" }) | ||
| public void installDependedOptionalPluginWithoutRestart() throws Throwable { | ||
| var optionalDependerJpi = rjr.createSyntheticPlugin(new SyntheticPlugin(OptionalDepender.class.getPackage()) | ||
| .header("Plugin-Dependencies", "variant:0,dependee:0;resolution:=optional")); |
There was a problem hiding this comment.
A lot easier to follow the test, not to say edit it!
| @@ -190,11 +190,10 @@ public void disableAlreadyDisabledPluginNotRestart() throws Exception { | |||
| @Ignore("TODO calling restart seems to break Surefire") | |||
There was a problem hiding this comment.
Clearly could stand to be rewritten.
|
I think the |
likely FWIW this PR triggered narrowing down a bit jenkinsci/avatar-plugin#18. I've tested and this PR doesn't resolve it though, but seems to be another issue related to dynamic loading. |
|
/label ready-for-merge This PR is now ready for merge, after the upcoming security release, we will merge it if there's no negative feedback. Thanks! |
See JENKINS-75232. The relevant
ExtensionListcode has not changed since it was introduced in #1673, but in the real-world case I saw the listener that is causing the issues comes from #3496.This PR requires jenkinsci/jenkins-test-harness#920 for the new test.
Testing done
A new automated test has been created to demonstrate the bug and test the fix. @jgreffe and I also tested the fix against the real-world plugin which triggered the bug (a proprietary CloudBees plugin).
Proposed changelog entries
Proposed upgrade guidelines
N/A
Desired reviewers
Before the changes are marked as
ready-for-merge: