Skip to content

[#154] Unregister the published customizer class on release() - #155

Open
vharseko wants to merge 5 commits into
OpenIdentityPlatform:masterfrom
vharseko:issue-154-release-unregisters-customizer
Open

vharseko wants to merge 5 commits into
OpenIdentityPlatform:masterfrom
vharseko:issue-154-release-unregisters-customizer

Conversation

@vharseko

@vharseko vharseko commented Oct 5, 2026

Copy link
Copy Markdown
Member

Fixes #154

Stacked on #132. The fix builds on the getGroovyScriptEngine() / initializeCustomizer() rework from #132, which is not on master yet, so the first four commits of this branch are #132's. Only the last commit, 2b0233b, belongs to this PR. Once #132 is merged, the branch will be rebased onto master and those commits drop out.

ScriptedConfiguration.release() dropped the published engine but left the class of its customizer script registered. The REST, CREST and SSH configurations register an ExpandoMetaClass on that class in createCustomizerScript (customizerClass.metaClass.customize << {…}). On Groovy 2.4.21 a registered metaclass keeps the class reachable, and with it the configuration's own GroovyClassLoader, every script it compiled, and the released configuration that owns the customize closure. So every facade dispose or configuration change of a REST, CREST or SSH connector leaked one loader. #132 covers only a customizer that fails.

Where Change
ScriptedConfiguration.initializeCustomizer() returns the customizer class (or null when there is none)
ScriptedConfiguration.getGroovyScriptEngine() keeps that class in publishedCustomizerClass, guarded by the configuration's monitor and set together with the published engine
ScriptedConfiguration.release() after the release closure has run, calls InvokerHelper.removeClass(publishedCustomizerClass) and clears the field. The REST, CREST and SQL overrides of release() all call super.release()

Test. ScriptedConfigurationTest.testReleaseUnregistersThePublishedCustomizerClass uses the stand-in from testFailedCustomizerClassIsUnregistered (now the shared helper registeringMetaClassOnCustomizer) with a customizer that succeeds. It checks that the metaclass is registered after getGroovyScriptEngine(), then calls release() and checks that ClassInfo.getClassInfo(clazz).getStrongMetaClass() is null. Without the release() change it fails on that last assertion.

Local run: mvn -pl OpenICF-groovy-connector,OpenICF-ssh-connector -am test. groovy-connector: 131 run, 0 failed. The skipped tests are the samples that need external systems (SQL/REST/CREST/Azure/ARM, Linux/Kerberos). SSHConfigurationCustomizerTest passed.

…nterned-string locks, racy lazy init

SQLUtil tested "instanceof Integer" twice, the second branch unreachable.
ContractITCase dereferenced a factory that was always null; it now uses
TestNG's ObjectFactoryImpl, and the unused nested ContractTestFactory
goes. BatchRemoteCache synchronised on an interned string literal, a
monitor shared with any other code using the same literal; it locks a
private object now. The double-checked lazy initialisation of
WebSocketConnectionGroup.operationContext,
ScriptedConfiguration.groovyScriptEngine and
TstStatefulConnectorConfig.executorService reads a non-volatile field
outside the lock; the fields are volatile now. FrameworkUtil's version
getter is synchronised like its setter, and the class-loader overload,
which reads a resource rather than the field, is readFrameworkVersion.
…tNG internals from ContractITCase

ScriptedConfiguration.getGroovyScriptEngine() stored the engine before
initializeCustomizer() ran: a failed customizer left an uncustomized engine
in place for good, and a concurrent caller could get it mid-customization
(OpenIdentityPlatform#148). The fast path also read the volatile field twice while release()
may null it in between (OpenIdentityPlatform#149). The field is now read once, and the engine
is published only after the customizer succeeds; the customizer's own
re-entrant calls get it from a lock-guarded field.

ContractITCase.createInstances instantiated org.testng.internal.ObjectFactoryImpl
on every call; TestNG 7.5 moved that class and 7.10.2 removed IObjectFactory.
It now calls the (String) constructor through plain reflection.

Fixes OpenIdentityPlatform#148
Fixes OpenIdentityPlatform#149
… in the test

Since a failed customizer is now retried on the next call, each failure
compiled a new customizer class. The REST, CREST and SSH configurations
register an ExpandoMetaClass on it, which keeps its GroovyClassLoader alive,
so a customizer that keeps failing leaked one loader per call.
initializeCustomizer() now removes the class with InvokerHelper.removeClass
when the customizer fails.

The comment on initializeCustomizer() states that the customizer runs under
the configuration's monitor and must not wait for another thread that uses
the engine.

testEngineIsHiddenFromOtherThreadsUntilCustomized took a 500 ms timeout as
proof that the second caller blocked; it now waits until that thread is
BLOCKED or done, so an early publish fails the test on every run.
A failed customizer is retried on the next call (OpenIdentityPlatform#148). When the failed
attempt registered release {} and the successful retry did not, release()
still ran the failed attempt's closure: SSH's customize never reset it,
and CREST's "release = null" wrote a private field that nothing reads.
Both now clear it with setReleaseClosure(null), the way REST's customize
already resets its closure, and CREST's unread field is gone.
release() dropped the engine but left the class of its customizer
registered. The REST, CREST and SSH configurations put an
ExpandoMetaClass on that class, which on Groovy 2.4.21 keeps the
class, its GroovyClassLoader with every compiled script, and the
released configuration alive: one loader leaked per facade dispose
or configuration change.

getGroovyScriptEngine() now keeps the customizer class of the
published engine, and release() removes it with
InvokerHelper.removeClass after the release closure has run.

Fixes OpenIdentityPlatform#154
@vharseko
vharseko requested a review from maximthomas October 5, 2026 13:39
@vharseko vharseko added bug Something isn't working java Pull requests that update java code connector:groovy Groovy connector connector:ssh SSH connector resource-leak Memory, class-loader, thread or handle leaks tests Test additions or fixes labels Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working connector:groovy Groovy connector connector:ssh SSH connector java Pull requests that update java code resource-leak Memory, class-loader, thread or handle leaks tests Test additions or fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ScriptedConfiguration.release() leaves the published engine's customizer class registered, pinning one GroovyClassLoader per release

1 participant