Skip to content

Concurrency 3.1#2577

Open
rzo1 wants to merge 26 commits intomainfrom
concurency
Open

Concurrency 3.1#2577
rzo1 wants to merge 26 commits intomainfrom
concurency

Conversation

@rzo1
Copy link
Copy Markdown
Contributor

@rzo1 rzo1 commented Apr 3, 2026

First version - think we can improve or re-work if needed.

@rzo1 rzo1 requested a review from jungm April 3, 2026 18:45
@rzo1 rzo1 marked this pull request as draft April 3, 2026 18:45
@rzo1 rzo1 changed the title Draft: Concurency 3.1 Draft: Concurrency 3.1 Apr 3, 2026
@rzo1 rzo1 marked this pull request as ready for review April 4, 2026 18:12
@rzo1 rzo1 changed the title Draft: Concurrency 3.1 Concurrency 3.1 Apr 4, 2026
// Per spec, the executor attribute may reference either a ManagedScheduledExecutorService
// or a plain ManagedExecutorService. When a plain MES is referenced, fall back to the
// default MSES for scheduling capability but preserve the MES's context service.
final ManagedScheduledExecutorServiceImpl mses = resolveMses(asynchronous.executor());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interestingly enough concurro completely disregargs the configured executor, claiming it is not to be used in the scheduled case according to the spec (though i cannot find where this behavior is described)
https://github.com/eclipse-ee4j/glassfish-concurro/blob/3.1.0/src/main/java/org/glassfish/concurro/cdi/asynchronous/AsynchronousInterceptor.java#L127-L132

openliberty seems to at least honor the contextservice that is attached to the used executor

// Extract method and target from InvocationContext for direct invocation.
// We must NOT reuse ctx.proceed() — InvocationContext's interceptor iterator
// is single-use, so subsequent proceed() calls would bypass TX/security interceptors.
final Method beanMethod = ctx.getMethod();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is likely not fixing what it is trying to fix as this returns the raw method and not the method on the CDI proxy that would run interceptors, pushed a test to prove this regression and working on a fix

rzo1 and others added 20 commits April 17, 2026 08:21
… 3.1

Add support for scheduled recurring async methods as defined in Jakarta
Concurrency 3.1. This addresses the largest block of TCK failures (28 tests)
by enabling cron-based scheduling of CDI @asynchronous methods via the new
runAt attribute.

- ScheduleHelper: maps @schedule annotations to API-provided CronTrigger,
  supports composite triggers (multiple schedules) and skipIfLateBy wrapping
- AsynchronousInterceptor: branches on runAt presence — one-shot path
  unchanged, new scheduled path uses ManagedScheduledExecutorService
- ManagedScheduledExecutorServiceImplFactory: adds lookup() with graceful
  fallback matching the ManagedExecutorServiceImplFactory pattern
Implement opt-in virtual thread support (Java 21+) across
ManagedThreadFactory, ManagedExecutorService, and
ManagedScheduledExecutorService as required by Jakarta Concurrency 3.1.

Runtime:
- VirtualThreadHelper: reflection-based Java 21 API access, graceful
  fallback on Java 17 (isSupported check, no multi-release JARs)
- ManagedThreadFactoryImpl: virtual path creates threads that do NOT
  implement ManageableThread (spec 3.4.4)
- All three factory classes accept virtual property

Deployment descriptor + annotation processing:
- Boolean virtual field added to DD model classes in openejb-jee
- Convert*Definitions pass virtual to Resource properties
- AnnotationDeployer reads virtual() from @Managed*Definition annotations

Tests skip gracefully on Java <21 via Assume.assumeTrue.
- ScheduleHelper: remove pass-through toMonths/toDaysOfWeek methods
  and unused DayOfWeek/Month imports
- ManagedScheduledExecutorServiceImplFactory: wire virtual flag into
  fallback thread factory creation, remove unused import
- VirtualThreadHelper: remove unused ofVirtualInterface variable
…duler

Verify that API-provided CronTrigger (a ZonedTrigger) works transparently
with ManagedScheduledExecutorServiceImpl via default bridge methods. Tests
confirm recurring scheduling and LastExecution with ZonedDateTime work
without any implementation changes.
The TCK beans call Asynchronous.Result.getFuture() inside scheduled
methods. The interceptor must call setFuture() before ctx.proceed()
for both void and non-void return types, otherwise getFuture() throws
IllegalStateException. Use Callable path for all scheduled methods
to ensure proper future lifecycle.
…d JNDI

Three fixes for @asynchronous(runAt=@schedule(...)) TCK compliance:

1. Stop trigger loop when method returns non-null value (per spec:
   "the method returns a non-null result value" ends the schedule).
   Uses AtomicReference to cancel ScheduledFuture from inside Callable.

2. Throw IllegalArgumentException for invalid executor JNDI names
   instead of silently falling back to default. Only default names
   get the graceful fallback.

3. Add beans.xml archive processor for Concurrency TCK deployments
   (OWB needs bean-discovery-mode="all" to auto-enable @priority
   interceptors).

4. Add TCK-style unit test covering CompletedResult, IncompleteFuture,
   CompletedExceptionally, VoidReturn, and InvalidJNDIName scenarios.
bean-discovery-mode=all caused deployment failures for some TCK WARs
by scanning too many classes. Switch to annotated mode which is the
CDI 4.0 default and sufficient for discovering annotated beans.
Extend TCK-style test coverage with multipleSchedules (composite
trigger with two @schedule annotations) and ignoresMaxAsync (concurrent
scheduled method invocations). Both pass — remaining TCK failures for
these tests are JNDI scoping issues with java:module/ executor names.
…tors

Strip java: prefix in ManagedScheduledExecutorServiceImplFactory.lookup()
fallback path to match how resources are registered via cleanUpName().
Without this, java:module/concurrent/ScheduledExecutorB lookups fail
because the resource is bound as module/concurrent/ScheduledExecutorB.

Add Arquillian test verifying both java:module/ and java:app/ scoped
@ManagedScheduledExecutorDefinition work with scheduled async methods.
Per spec, @asynchronous(executor=..., runAt=@schedule(...)) may reference
a plain ManagedExecutorService, not just a ManagedScheduledExecutorService.
When MSES lookup fails, verify the executor exists as MES and fall back to
the default MSES for scheduling capability.
…tion

Two fixes for the last Web-profile TCK failures:

1. Context propagation: when executor is a plain MES (not MSES),
   extract the MES's ContextServiceImpl and compose a temporary MSES
   that uses the MES's context service with the default MSES's thread
   pool. This preserves third-party context propagation (e.g. StringContext).

2. Thread pool starvation: replace mses.schedule(Callable, Trigger)
   with a manual trigger loop that schedules directly on the delegate
   ScheduledExecutorService. This avoids TriggerTask's double context
   wrapping and thread consumption between trigger fires.
Per spec, scheduled async methods are not subject to maxAsync
constraints. Use the default MSES's thread pool for the trigger loop
instead of the referenced executor's pool. This prevents thread
starvation when maxAsync threads are busy with blocking tasks.
Replace !eefull with web tag to properly exclude Full/EJB profile
tests. The TCK 3.1.1 uses JUnit 5 @tag annotations: @web has tags
web+platform, @platform has only platform. Filtering on web includes
Core, Standalone, and Web tests while excluding Platform-only (Full).
- Add qualifier field (List<String>) to ContextService, ManagedExecutor,
  ManagedScheduledExecutor, ManagedThreadFactory DD model classes
- Update SXC JAXB accessors to parse <qualifier> and <virtual> XML
  elements (virtual was in the model but missing from SXC parsers)
- Fix NPE in Convert*Definitions when <context-service-ref> is absent
  in deployment descriptor — defaults to java:comp/DefaultContextService
- Add unit tests for null context service fallback and qualifier model
- Add Arquillian test deploying WAR with web.xml containing <virtual>
ForkJoinWorkerThread extends Thread (platform) and cannot be virtual.
Fall back to a platform ManagedForkJoinWorkerThread instead of throwing
UnsupportedOperationException. The TCK expects ForkJoinPool to function
with virtual factories — the worker threads are platform but the pool
still operates correctly.
m>
Register concurrency resources (ManagedExecutorService, ManagedScheduledExecutorService,
ManagedThreadFactory, ContextService) as CDI beans with qualifier support per
Concurrency 3.1 spec Section 5.4.1.

- ConcurrencyCDIExtension: CDI extension that observes AfterBeanDiscovery and creates
  synthetic ApplicationScoped beans for resources with qualifiers. Also registers
  default beans (@Default/@Any) for all four concurrency resource types.
- AnnotationDeployer: Extract qualifiers() from @ManagedExecutorDefinition,
  @ManagedScheduledExecutorDefinition, @ManagedThreadFactoryDefinition,
  @ContextServiceDefinition annotations into JEE model objects.
- Convert*Definitions: Pass Qualifiers as comma-separated Resource property.
- OptimizedLoaderService: Register ConcurrencyCDIExtension alongside JMS2CDIExtension.

TCK Web profile: 196/196 passing (0 failures, 0 errors).
Per Concurrency 3.1 spec: "When running on Java SE 17, the true value
behaves the same as the false value and results in platform threads
being created rather than virtual threads."

Previously, virtual=true unconditionally called VirtualThreadHelper
methods which throw UnsupportedOperationException on Java 17. Now
checks VirtualThreadHelper.isSupported() first and falls through to
platform thread creation when virtual threads are unavailable.
…e headers

Replace ctx.proceed() with direct Method.invoke() in scheduled async
re-executions. InvocationContext's interceptor iterator is single-use,
so re-calling proceed() would bypass TX/security interceptors after
the first scheduled execution. Context propagation is handled by
ContextService.enter/exit.

Also add Apache License headers to TCK resource files to fix RAT check.
rzo1 added 3 commits April 17, 2026 09:29
Re-dispatch each scheduled firing through the CDI interceptor proxy so
TX, security, and application interceptors run on every run. The prior
code called beanMethod.invoke(target, ...) directly and bypassed the
chain; InvocationContext.proceed() cannot be reused because OWB's
implementation advances an index that never resets, so a second call
falls through to invoking the target method and skips every lower-
priority interceptor.

OWB coupling is isolated in ScheduledAsyncInvoker, which reuses the
existing OwbInterceptorProxy when the bean is normal-scoped and
otherwise builds a fresh interceptor proxy via
InterceptorResolutionService.createProxiedInstance. Re-entry detection
uses a precise (method, target) key, restored rather than cleared on
exit so nested @asynchronous calls on different methods or targets do
not collide.

Also routes scheduled firings to the executor named in
@asynchronous(executor=...) via mses.getDelegate() rather than silently
switching to DefaultManagedScheduledExecutorService, so custom thread
factories, priorities, and virtual-thread settings take effect.

Adds tests for interceptor ordering across firings and for routing the
firing thread to the requested executor vs. the default pool.
ManagedScheduledExecutorServiceImplFactory used to resolve its thread
factory via ThreadFactories.findThreadFactory, which calls the no-arg
ManagedThreadFactoryImpl constructor reflectively. That constructor
hard-codes virtual=false, so @ManagedScheduledExecutorDefinition(
virtual=true) was silently ignored unless the reflective lookup failed
and the catch block instantiated the factory directly.

Short-circuit the default class name to construct the factory with the
configured virtual flag, matching the pattern already used in
ManagedExecutorServiceImplFactory. Adds regression tests asserting that
virtual=true on the scheduled factory yields threads that do not
implement ManageableThread (per Concurrency 3.1 §3.4.4) and that the
default path still produces platform threads.
Introduces @EnabledForJreRange plus a small JreConditionRule so JUnit 4
tests can declare a JRE feature-version range at the method level,
mirroring the semantics of JUnit Jupiter's EnabledForJreRange. The rule
reads the annotation from the test description and throws
AssumptionViolatedException when the current JRE is outside the range,
so Surefire reports out-of-range methods as skipped.

Migrates VirtualThreadHelperTest off imperative Assume.assumeTrue /
Assume.assumeFalse calls: Java 21+ cases use @EnabledForJreRange(min=21),
the UnsupportedOperationException cases use @EnabledForJreRange(max=20).
rzo1 added 3 commits April 17, 2026 09:41
Mirrors the failing TCK scenario ManagedScheduledExecutorDefinitionWebTests.
testScheduledAsynchIgnoresMaxAsync in-process via ApplicationComposer: a
custom ManagedScheduledExecutorService with maxAsync=1 is saturated with a
blocking submit, then an @asynchronous(runAt=@schedule) method targeting
that executor is invoked. Per Concurrency 3.1 the scheduled firing must not
be subject to maxAsync, but after routing scheduled firings onto
mses.getDelegate() the firing is queued behind the saturating task because
the delegate's ScheduledThreadPoolExecutor has corePoolSize = maxAsync.
Add a secondary ScheduledExecutorService to every
ManagedScheduledExecutorServiceImpl, dedicated to dispatching
@asynchronous(runAt=@schedule(...)) firings. The secondary pool shares
the primary's ManagedThreadFactory (preserving naming, virtual flag,
priority, and context) but is sized independently via a new
ScheduledAsyncCore resource property (default 5). The interceptor now
triggers on the secondary pool, so scheduled firings are no longer
throttled by the primary pool's corePoolSize=maxAsync limit — satisfying
Concurrency 3.1 §3.1 "Scheduled asynchronous methods are treated similar
to other scheduled tasks in that they are not subject to max-async
constraints" without losing the clause from PR #2577 review that firings
must run on the executor named in @asynchronous(executor=).

The plain-MES fallback wrapper borrows the default MSES's secondary so
short-lived per-invocation wrappers do not leak a fresh pool each time;
an ownership flag on the 4-arg constructor controls which pools
destroyResource() shuts down.

Also fix a pre-existing race in CUTriggerScheduledFuture: cancel() can
race ahead of the recursive scheduleNextRun(), leaving futureRef
pointing at the just-completed execution so the underlying future
reports done rather than cancelled. isCancelled() now falls back to the
TriggerTask's cancelled flag.

Tests:
- ScheduledAsyncCustomFactoryTest — scheduled firing runs on a
  ManageableThread from the custom MSES's factory, not the primary pool.
- ManagedScheduledExecutorSecondaryPoolLifecycleTest — owned secondary
  is shut down on destroyResource(); borrowed secondary is not.

TCK Web profile back to 196/0/0/14.
AnnotationDeployer#buildManaged{Executor,ScheduledExecutor,ThreadFactory}Definition
was unconditionally overwriting deployment-descriptor values (name,
context, hungTaskThreshold, maxAsync, virtual, priority) with annotation
values whenever both sources shared a resource name. That violates the
Jakarta EE platform DD-wins rule (§EE.5.2.5); only qualifier was guarded.

Guard every field assignment with a null check, mirroring the existing
buildContextServiceDefinition pattern, so DD values are preserved and the
annotation only fills attributes the descriptor left unset.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants