Skip to content

Commit 4ee90ed

Browse files
committed
Add regression test for scheduled async maxAsync isolation
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.
1 parent f6cfa55 commit 4ee90ed

1 file changed

Lines changed: 125 additions & 0 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.openejb.cdi.concurrency;
18+
19+
import jakarta.enterprise.concurrent.Asynchronous;
20+
import jakarta.enterprise.concurrent.ManagedScheduledExecutorDefinition;
21+
import jakarta.enterprise.concurrent.ManagedScheduledExecutorService;
22+
import jakarta.enterprise.concurrent.Schedule;
23+
import jakarta.enterprise.context.ApplicationScoped;
24+
import jakarta.inject.Inject;
25+
import org.apache.openejb.jee.EnterpriseBean;
26+
import org.apache.openejb.jee.SingletonBean;
27+
import org.apache.openejb.junit.ApplicationComposer;
28+
import org.apache.openejb.testing.Module;
29+
import org.junit.Test;
30+
import org.junit.runner.RunWith;
31+
32+
import javax.naming.InitialContext;
33+
import java.util.concurrent.CompletableFuture;
34+
import java.util.concurrent.CountDownLatch;
35+
import java.util.concurrent.TimeUnit;
36+
import java.util.concurrent.atomic.AtomicInteger;
37+
38+
import static org.junit.Assert.assertEquals;
39+
import static org.junit.Assert.assertNotNull;
40+
import static org.junit.Assert.assertTrue;
41+
42+
/**
43+
* Verifies Concurrency 3.1 §3.1: "Scheduled asynchronous methods are treated similar to
44+
* other scheduled tasks in that they are not subject to max-async constraints."
45+
*
46+
* <p>Mirrors the TCK test
47+
* {@code ManagedScheduledExecutorDefinitionWebTests.testScheduledAsynchIgnoresMaxAsync}:
48+
* a custom {@link ManagedScheduledExecutorService} is saturated with regular async
49+
* submissions that fill all {@code maxAsync} slots; a scheduled {@code @Asynchronous}
50+
* firing on the same executor must still execute and complete, rather than queueing
51+
* behind the saturating workload.
52+
*/
53+
@RunWith(ApplicationComposer.class)
54+
public class ScheduledAsyncMaxAsyncIsolationTest {
55+
56+
private static final String MSES_JNDI = "java:module/maxasync/limitedMSES";
57+
private static final int MAX_ASYNC = 1;
58+
59+
@Inject
60+
private SaturatedBean bean;
61+
62+
@Module
63+
public EnterpriseBean ejb() {
64+
return new SingletonBean(DummyEjb.class).localBean();
65+
}
66+
67+
@Module
68+
public Class<?>[] beans() {
69+
return new Class<?>[]{SaturatedBean.class};
70+
}
71+
72+
@Test
73+
public void scheduledFiringBypassesMaxAsync() throws Exception {
74+
final ManagedScheduledExecutorService mses = InitialContext.doLookup(MSES_JNDI);
75+
76+
// Saturate every maxAsync slot with tasks that block until the test releases them.
77+
// With maxAsync=1 the underlying ScheduledThreadPoolExecutor has corePoolSize=1, so a
78+
// single blocking submission occupies the only worker thread.
79+
final CountDownLatch saturationStarted = new CountDownLatch(MAX_ASYNC);
80+
final CountDownLatch release = new CountDownLatch(1);
81+
for (int i = 0; i < MAX_ASYNC; i++) {
82+
mses.submit(() -> {
83+
saturationStarted.countDown();
84+
try {
85+
release.await(20, TimeUnit.SECONDS);
86+
} catch (final InterruptedException e) {
87+
Thread.currentThread().interrupt();
88+
}
89+
});
90+
}
91+
assertTrue("Saturating tasks should occupy all maxAsync slots before the scheduled firing is triggered",
92+
saturationStarted.await(5, TimeUnit.SECONDS));
93+
94+
try {
95+
// A scheduled firing on the same executor must not be blocked by the saturating
96+
// workload. With a one-second cron and a 15-second deadline there is ample headroom
97+
// unless the firing is queued behind the maxAsync core threads.
98+
final AtomicInteger counter = new AtomicInteger();
99+
final CompletableFuture<Integer> future = bean.scheduledFire(counter);
100+
final Integer result = future.get(15, TimeUnit.SECONDS);
101+
102+
assertNotNull("Scheduled firing must complete despite saturated maxAsync slots", result);
103+
assertEquals("Scheduled firing should have executed exactly once", 1, counter.get());
104+
} finally {
105+
release.countDown();
106+
}
107+
}
108+
109+
@ManagedScheduledExecutorDefinition(name = MSES_JNDI, maxAsync = MAX_ASYNC)
110+
@ApplicationScoped
111+
public static class SaturatedBean {
112+
113+
@Asynchronous(executor = MSES_JNDI, runAt = @Schedule(cron = "* * * * * *"))
114+
public CompletableFuture<Integer> scheduledFire(final AtomicInteger counter) {
115+
final int count = counter.incrementAndGet();
116+
final CompletableFuture<Integer> future = Asynchronous.Result.getFuture();
117+
future.complete(count);
118+
return future;
119+
}
120+
}
121+
122+
@jakarta.ejb.Singleton
123+
public static class DummyEjb {
124+
}
125+
}

0 commit comments

Comments
 (0)