oscerd commented on code in PR #739:
URL: https://github.com/apache/camel-karaf/pull/739#discussion_r3870770117
##########
core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiTypeConverter.java:
##########
@@ -279,28 +335,59 @@ public Set<ClassLoader> getClassLoaders() {
throw new RuntimeCamelException("Error loading CoreTypeConverter
due: " + e.getMessage(), e);
}
- // Load the type converters the tracker has been tracking
- // Here we need to use the ServiceReference to check the ranking
- ServiceReference<TypeConverterLoader>[] serviceReferences =
this.tracker.getServiceReferences();
- if (serviceReferences != null) {
- ArrayList<ServiceReference<TypeConverterLoader>> servicesList =
- new ArrayList<>(Arrays.asList(serviceReferences));
- // Just make sure we install the high ranking fallback converter
at last
- Collections.sort(servicesList);
- for (ServiceReference<TypeConverterLoader> sr : servicesList) {
- try {
- LOG.debug("loading type converter from bundle: {}",
sr.getBundle().getSymbolicName());
-
((TypeConverterLoader)this.tracker.getService(sr)).load(answer);
- } catch (Throwable t) {
- throw new RuntimeCamelException("Error loading type
converters from service: " + sr + " due: " + t.getMessage(), t);
- }
+ // Load the type converters the tracker has been tracking. These come
from our own map rather than from
+ // tracker.getServiceReferences()/getService(): this runs while
holding this instance's monitor, and
+ // calling back into the tracker from here is what would establish a
lock ordering against the framework.
+ List<ServiceReference<TypeConverterLoader>> servicesList = new
ArrayList<>(trackedLoaders.keySet());
+ // Just make sure we install the high ranking fallback converter at
last
+ Collections.sort(servicesList);
+ for (ServiceReference<TypeConverterLoader> sr : servicesList) {
+ TypeConverterLoader loader = trackedLoaders.get(sr);
+ if (loader == null) {
+ // unregistered between the snapshot and here
+ continue;
+ }
+ try {
+ LOG.debug("loading type converter from bundle: {}",
sr.getBundle().getSymbolicName());
+ loader.load(answer);
+ } catch (Throwable t) {
+ throw new RuntimeCamelException("Error loading type converters
from service: " + sr + " due: " + t.getMessage(), t);
}
}
+ replayProgrammaticRegistrations(answer);
+
LOG.trace("Created TypeConverter: {}", answer);
return answer;
}
+ /**
+ * Re-applies everything that was registered through this facade rather
than by a
+ * {@link TypeConverterLoader}, in the order it was originally applied.
+ */
+ private void replayProgrammaticRegistrations(DefaultTypeConverter
registry) {
+ if (programmaticRegistrations.isEmpty()) {
+ return;
+ }
+ LOG.debug("Replaying {} programmatic registration(s) onto the rebuilt
type converter registry",
+ programmaticRegistrations.size());
+ for (Consumer<TypeConverterRegistry> registration :
programmaticRegistrations) {
+ registration.accept(registry);
+ }
+ }
+
+ /**
+ * Applies a registration to the current delegate and remembers it, so
that discarding the delegate does not
+ * discard the registration with it.
+ */
+ private void register(Consumer<TypeConverterRegistry> registration) {
+ // apply first: a registration the delegate rejects is not one worth
replaying. Note getDelegate() may
+ // build the registry here, which replays the list as it stands - this
registration is added after, so
+ // it cannot be applied twice
+ registration.accept(getDelegate());
+ programmaticRegistrations.add(registration);
+ }
Review Comment:
Fixed exactly as you suggested — `register()` is `private synchronized`, and
`removeTypeConverter()` has the same treatment.
You are right that the comment defended the wrong half. It argued the
registration cannot be applied *twice*; the hole was that it can be applied
*zero* times. I have rewritten it to say what the monitor is actually for.
On testing it: `registrationCannotInterleaveWithARebuild` stalls inside an
overridden `createRegistry()` and asserts a concurrent `addTypeConverter`
cannot proceed, then that it lands on the live registry. Being straight about
its limits — it passes against the previous head too, because the old
`register()` still went through a `synchronized getDelegate()` and blocked
there anyway. So it is a forward guard, not a regression test. Reproducing the
real interleave needs a registry that can be stalled inside its own
`addTypeConverter`, the way your throwaway did; I did not build one, and said
so in the commit message rather than let the test imply more coverage than it
has.
_Claude Code on behalf of Andrea Cosentino_
##########
core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiTypeConverter.java:
##########
@@ -112,6 +150,16 @@ public void
removedService(ServiceReference<TypeConverterLoader> serviceReferenc
this.delegate = null;
Review Comment:
Split out as you asked: **#743**, which covers both this lost invalidation
and the eager-stop race from the `getDelegate()` thread, since they are the
same shape — mutation of an instance callers already hold, performed outside
the monitor that publishes it — and a fix for one constrains the fix for the
other.
Your epoch-counter sketch is in there, along with your reproduction
(stalling at the end of `createRegistry()`, `assertSame(built, afterRemoval)`
passing) and the point that #739 widens the window rather than narrowing it,
since the monitor is now held across the whole rebuild.
Nothing about it is fixed in this PR.
_Claude Code on behalf of Andrea Cosentino_
##########
core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiTypeConverter.java:
##########
@@ -57,6 +61,19 @@ public class OsgiTypeConverter extends ServiceSupport
implements TypeConverter,
private CamelContext camelContext;
private final Injector injector;
private final ServiceTracker<TypeConverterLoader, Object> tracker;
+ /**
+ * The loaders the tracker has handed us, kept here rather than read back
from the tracker: resolving them
+ * through the tracker inside {@link #createRegistry()} would mean calling
into the ServiceTracker and the
+ * framework while holding this instance's monitor.
+ */
+ private final Map<ServiceReference<TypeConverterLoader>,
TypeConverterLoader> trackedLoaders
+ = new ConcurrentHashMap<>();
+ /**
+ * Registrations made through this facade rather than by a {@link
TypeConverterLoader}, in the order they were
+ * made, so a rebuilt registry can be brought back to the same state.
Discarding the delegate would otherwise
+ * drop them with no way to get them back.
+ */
+ private final List<Consumer<TypeConverterRegistry>>
programmaticRegistrations = new CopyOnWriteArrayList<>();
Review Comment:
Agreed, and it is my regression — the replay list introduced it. Taken your
suggestion.
`programmaticRegistrations` is now a keyed, insertion-ordered
`LinkedHashMap<Object, Consumer<TypeConverterRegistry>>`. The key is what the
registration is *about*:
- `TypeConvertible` for `addTypeConverter` and `addConverter`
- the contributed instance for `addBulkTypeConverters`,
`addFallbackTypeConverter` and `addTypeConverters(Object)`
- a sentinel per setter, so repeated `setTypeConverterExists` calls replace
rather than stack
That fixes all three things you named in one move: growth, retention, and
the `removeTypeConverter` ordering question. It also answers the Blueprint
refresh point — re-registering the same conversion now replaces the entry
instead of appending a duplicate (`reRegisteringTheSameConversionReplaces`).
You are right that `addTypeConverters(Object)` has no natural key. Keying on
the contributed object itself is the best available: a refresh that hands over
an equal instance replaces, and one that hands over a genuinely different
object is a genuinely different registration. It does not bound the reflective
`@Converter` re-scan on rebuild, which stays proportional to the number of
distinct contributed objects.
Tests: `addRemovePairsDoNotAccumulate` reproduces your figure — 1000 entries
against the previous head, 0 now.
_Claude Code on behalf of Andrea Cosentino_
##########
core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiTypeConverter.java:
##########
@@ -173,27 +223,30 @@ public <T> T tryConvertTo(Class<T> type, Object value) {
@Override
public void addTypeConverter(Class<?> toType, Class<?> fromType,
TypeConverter typeConverter) {
- getDelegate().addTypeConverter(toType, fromType, typeConverter);
+ register(registry -> registry.addTypeConverter(toType, fromType,
typeConverter));
}
@Override
public void addTypeConverters(Object typeConverters) {
- getDelegate().addTypeConverters(typeConverters);
+ register(registry -> registry.addTypeConverters(typeConverters));
}
@Override
public void addBulkTypeConverters(BulkTypeConverters bulkTypeConverters) {
- getDelegate().addBulkTypeConverters(bulkTypeConverters);
+ register(registry ->
registry.addBulkTypeConverters(bulkTypeConverters));
}
@Override
public boolean removeTypeConverter(Class<?> toType, Class<?> fromType) {
- return getDelegate().removeTypeConverter(toType, fromType);
+ boolean removed = getDelegate().removeTypeConverter(toType, fromType);
+ // replayed as well, so a rebuild reproduces the sequence rather than
resurrecting the converter
+ programmaticRegistrations.add(registry ->
registry.removeTypeConverter(toType, fromType));
+ return removed;
}
Review Comment:
Both fixed, by the keyed collection in the `programmaticRegistrations`
thread.
- **Appends an inverse instead of pruning** — `removeTypeConverter` now
deletes the matching `TypeConvertible` entry. Same end state on replay, and the
converter is actually released rather than staying reachable through the add
lambda.
- **Appends even when nothing was removed** — a removal for a pair that was
never registered now leaves the collection untouched. `removed` is still
returned to the caller, it just no longer drives a retained entry. Test:
`removingAConversionThatWasNeverRegisteredDoesNotAccumulate`.
The lost-registration window you noted here is closed by the same
`synchronized` as on `register()` — `removeTypeConverter` is synchronized too,
so its `getDelegate()` and its prune are one step.
_Claude Code on behalf of Andrea Cosentino_
##########
core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiTypeConverter.java:
##########
@@ -121,6 +169,8 @@ protected void doStart() throws Exception {
protected void doStop() throws Exception {
this.tracker.close();
this.trackerOpened = false;
+ // close() calls removedService for everything still tracked, this
only makes the end state explicit
+ this.trackedLoaders.clear();
ServiceHelper.stopService(this.delegate);
this.delegate = null;
}
Review Comment:
Fixed, and thank you for reproducing it — I had not thought about the
restart path at all.
`doStop()` now clears `programmaticRegistrations` alongside
`trackedLoaders`, inside a `synchronized (this)` block so it cannot race a
rebuild. The `tracker.close()` call stays outside the monitor, since that is a
framework call and putting it under the lock is the thing we spent the first
round removing.
Test added next to `removedServiceReleasesTheService` as you suggested:
`restartDoesNotReplayThePreviousLifecycle` runs `start()` -> `addTypeConverter`
-> `stop()` -> `start()` and asserts both that nothing is retained and that the
lookup comes back `null`. It fails against the previous head.
_Claude Code on behalf of Andrea Cosentino_
##########
core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiTypeConverter.java:
##########
@@ -67,27 +84,36 @@ public OsgiTypeConverter(BundleContext bundleContext,
CamelContext camelContext,
this.tracker = new ServiceTracker<>(bundleContext,
TypeConverterLoader.class.getName(), this);
}
- private void ensureTrackerOpen() {
+ private synchronized void ensureTrackerOpen() {
if (!trackerOpened) {
tracker.open();
trackerOpened = true;
}
}
Review Comment:
Filed as **#744** rather than grown into this PR, as you suggested.
Good catch on the retry path — `ServiceTracker.open()` assigning `tracked`
before calling `trackInitial()` means the flag-based retry is not just useless
but actively misleading, since the second call returns as though it had
succeeded. Worth noting in the issue that the fix probably is not the flag: a
single loader throwing out of `trackInitial()` aborting the rest of the initial
set is the more interesting half.
The `synchronized` stays here for the reason in the thread above.
_Claude Code on behalf of Andrea Cosentino_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]