pat-lego opened a new pull request, #89:
URL: https://github.com/apache/sling-org-apache-sling-models-impl/pull/89
## Problem
`OSGiServiceInjector` calls `BundleContext.getServiceReferences()` and
`BundleContext.getService()` for **every** `@OSGiService` injection of
**every** model instance, and `ungetService()` again from the model's disposal
callback. Every one of these calls goes through the framework service registry.
On Felix, `getServiceReferences()` additionally gets, invokes and ungets every
registered `FindHook` on each call.
When a page instantiates many models concurrently, request threads end up
serialised on the service registry. A thread dump taken under load on a Felix 7
/ JDK 21 publish instance showed 79 of 180 request threads inside
`ServiceRegistry.getService()` called from `Felix.getServiceReferences()` (i.e.
while obtaining a `FindHook`). 73 of them were parked on a
`ServiceRegistry$ServiceHolder` `CountDownLatch`, and 64 came from
`OSGiServiceInjector`:
```
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007f3803288> (a
java.util.concurrent.CountDownLatch$Sync)
at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:230)
at
org.apache.felix.framework.ServiceRegistry.getService(ServiceRegistry.java:380)
at org.apache.felix.framework.Felix.getService(Felix.java:3984)
at org.apache.felix.framework.Felix.getServiceReferences(Felix.java:3895)
<-- obtaining the FindHook
at
org.apache.felix.framework.Felix.getAllowedServiceReferences(Felix.java:3947)
at
org.apache.felix.framework.BundleContextImpl.getServiceReferences(BundleContextImpl.java:414)
at
org.apache.sling.models.impl.injectors.OSGiServiceInjector.getService(OSGiServiceInjector.java:124)
at
org.apache.sling.models.impl.injectors.OSGiServiceInjector.getValue(OSGiServiceInjector.java:198)
at
org.apache.sling.models.impl.injectors.OSGiServiceInjector.getValue(OSGiServiceInjector.java:111)
at
org.apache.sling.models.impl.ModelAdapterFactory.injectElementInternal(ModelAdapterFactory.java:600)
at
org.apache.sling.models.impl.ModelAdapterFactory.injectElement(ModelAdapterFactory.java:535)
at
org.apache.sling.models.impl.ModelAdapterFactory.createObject(ModelAdapterFactory.java:754)
at
org.apache.sling.models.impl.ModelAdapterFactory.internalCreateModel(ModelAdapterFactory.java:409)
at
org.apache.sling.models.impl.ModelAdapterFactory.getAdapter(ModelAdapterFactory.java:219)
at
org.apache.sling.adapter.internal.AdapterManagerImpl.getAdapter(AdapterManagerImpl.java:121)
at
org.apache.sling.api.adapter.SlingAdaptable.adaptTo(SlingAdaptable.java:104)
...
```
Other request threads in the same dump were creating a new `ServiceHolder`
(and latch) for the hook. Felix only does this when the previous usage of the
hook has been fully released, so the holder is torn down and rebuilt over and
over:
```
java.lang.Thread.State: RUNNABLE
at java.util.concurrent.CountDownLatch.<init>(CountDownLatch.java:199)
at
org.apache.felix.framework.ServiceRegistry$ServiceHolder.<init>(ServiceRegistry.java:786)
at
org.apache.felix.framework.ServiceRegistry.getService(ServiceRegistry.java:357)
at org.apache.felix.framework.Felix.getService(Felix.java:3984)
at org.apache.felix.framework.Felix.getServiceReferences(Felix.java:3895)
at
org.apache.felix.framework.Felix.getAllowedServiceReferences(Felix.java:3947)
at
org.apache.felix.framework.BundleContextImpl.getServiceReferences(BundleContextImpl.java:414)
at
org.apache.sling.models.impl.injectors.OSGiServiceInjector.getService(OSGiServiceInjector.java:124)
...
```
The set of services a model can inject only changes when a service is
registered, modified or unregistered, so doing a registry lookup per injection
is avoidable.
## Change
This PR adds a package-private `OSGiServiceCache`, used by
`OSGiServiceInjector`:
- **References** are cached per *(requesting bundle context, service class,
filter)* and sorted by descending service ranking once. Empty results are
cached too, so repeated lookups of absent `@Optional` services do not hit the
registry either. Any `ServiceEvent` (`REGISTERED`, `MODIFIED`, `UNREGISTERING`)
for a service evicts all cached lookups for each of its `objectClass` names.
The cache uses an `AllServiceListener`, so events are not filtered by
class-space compatibility. Lookups with an invalid filter are not cached.
- **Service objects** are obtained once per *(requesting bundle context,
reference)* and kept, similar to a static DS reference. They are released when
the service is unregistered, when the requesting (model) bundle stops
(`SynchronousBundleListener`, `STOPPING`), or when the injector is deactivated.
Cached services are no longer registered in the model's disposal callback.
Services obtained outside the cache still are.
- **Races:** the per-class map is resolved before the registry is queried,
so a result computed concurrently with an eviction ends up in the evicted map
and is never served. If a cached reference turns out to be unregistered
(`ServiceReference.getBundle() == null`), it is evicted and released on first
use.
- **Fallback:** while the cache is not active (before activation or after
deactivation), the injector behaves exactly as before: lookup, `getService` and
`ungetService` via the disposal callback.
No API or exported-package change (bnd baseline passes).
### Behaviour change to discuss
- Bundle-scoped `ServiceFactory` services now keep one usage per model
bundle for as long as the service is registered. Previously, the usage count
went to 0 after each model's disposal callback, which could trigger
`ServiceFactory.ungetService` (and, for delayed DS components, deactivation)
between requests. The injector keeps using `BundleContext.getService`, so
prototype-scope semantics are unchanged.
- If a `FindHook` changes its visibility decisions **without** a service
event for the affected service class, and the requesting bundle is not stopped,
the change only shows up after the next such event. Reviewers may know of
setups where this matters, such as dynamic region or subsystem filtering.
## Testing
- `mvn verify` on JDK 21: 301 unit tests and 28 integration tests pass;
spotless and bnd baseline pass.
- New `OSGiServiceCacheTest` (13 tests) covers lookup-once, ranking order,
negative caching, invalid filters, eviction by all object classes, per-context
service caching, unregistered-while-obtained, `UNREGISTERING` release, bundle
`STOPPING` and `clear()`.
- `OSGiInjectionTest` has 6 new tests: caching across model instances,
collection injection, eviction on `REGISTERED`, release on `UNREGISTERING`, a
stale cached reference, and deactivation fallback.
- I also ran a local microbenchmark: Felix 7.0.5 with Felix SCR, one no-op
delayed-DS `FindHook`, 3 candidate services, 64 threads, and 50 injections per
"request" before the disposal-callback `ungetService`. It measured about 19-21
µs per injection with the current lookup+get path and about 0.1-0.6 µs with the
cache (single thread: about 1.3-2.0 µs vs. about 0.01 µs). This synthetic setup
did not reproduce the latch parking seen in the production dump, which depends
on how expensive it is to obtain the real hooks. Treat these numbers as an
indication of per-injection registry overhead only.
## Notes
There is no SLING Jira issue for this yet. I'm opening this as a draft to
discuss the approach and am happy to create the issue and adjust naming or
conventions.
--
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]