Skip to content

Commit f7c20a0

Browse files
committed
Add FrameworkQuiesce listener for waiting until framework events settle
Signed-off-by: Stefan Bischof <stbischof@bipolis.org>
1 parent 4af76d3 commit f7c20a0

3 files changed

Lines changed: 254 additions & 0 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*******************************************************************************
2+
* Copyright (c) Contributors to the Eclipse Foundation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*******************************************************************************/
18+
19+
package org.osgi.test.common.quiesce;
20+
21+
import java.time.Duration;
22+
import java.util.concurrent.locks.Condition;
23+
import java.util.concurrent.locks.ReentrantLock;
24+
25+
import org.osgi.framework.AllServiceListener;
26+
import org.osgi.framework.BundleContext;
27+
import org.osgi.framework.BundleEvent;
28+
import org.osgi.framework.ServiceEvent;
29+
import org.osgi.framework.SynchronousBundleListener;
30+
31+
/**
32+
* Waits for the OSGi framework to become quiet, i.e. no bundle or service
33+
* events are fired for a specified period. Implements both
34+
* {@link SynchronousBundleListener} and {@link AllServiceListener} so the
35+
* caller controls the scope by choosing which listener to register.
36+
*/
37+
public class FrameworkQuiesce implements SynchronousBundleListener, AllServiceListener {
38+
39+
private final ReentrantLock lock = new ReentrantLock();
40+
private final Condition quiet = lock.newCondition();
41+
private int eventCount;
42+
private long lastEventNanos;
43+
44+
private FrameworkQuiesce() {
45+
this.lastEventNanos = System.nanoTime();
46+
}
47+
48+
@Override
49+
public void bundleChanged(BundleEvent event) {
50+
onEvent();
51+
}
52+
53+
@Override
54+
public void serviceChanged(ServiceEvent event) {
55+
onEvent();
56+
}
57+
58+
private void onEvent() {
59+
lock.lock();
60+
try {
61+
eventCount++;
62+
lastEventNanos = System.nanoTime();
63+
quiet.signalAll();
64+
} finally {
65+
lock.unlock();
66+
}
67+
}
68+
69+
private void doWaitForQuiet(Duration quietPeriod, Duration timeout)
70+
throws InterruptedException, QuiesceTimeoutException {
71+
long quietNanos = quietPeriod.toNanos();
72+
long deadlineNanos = System.nanoTime() + timeout.toNanos();
73+
lock.lock();
74+
try {
75+
for (;;) {
76+
long now = System.nanoTime();
77+
if (now >= deadlineNanos) {
78+
throw new QuiesceTimeoutException(quietPeriod, timeout, eventCount);
79+
}
80+
long quietUntil = lastEventNanos + quietNanos;
81+
if (now >= quietUntil) {
82+
return;
83+
}
84+
long waitNanos = Math.min(quietUntil, deadlineNanos) - now;
85+
quiet.awaitNanos(waitNanos);
86+
}
87+
} finally {
88+
lock.unlock();
89+
}
90+
}
91+
92+
private int getEventCount() {
93+
lock.lock();
94+
try {
95+
return eventCount;
96+
} finally {
97+
lock.unlock();
98+
}
99+
}
100+
101+
/**
102+
* Registers for both bundle and service events, waits for quiet, then
103+
* unregisters.
104+
*
105+
* @param bc the bundle context to register listeners on
106+
* @param quietPeriod required silence duration
107+
* @param timeout maximum total wait time
108+
* @return the number of events received during the wait
109+
* @throws InterruptedException if the waiting thread is interrupted
110+
* @throws QuiesceTimeoutException if {@code timeout} expires before the
111+
* quiet period is reached
112+
*/
113+
public static int waitForQuiet(BundleContext bc, Duration quietPeriod, Duration timeout)
114+
throws InterruptedException, QuiesceTimeoutException {
115+
FrameworkQuiesce listener = new FrameworkQuiesce();
116+
bc.addBundleListener(listener);
117+
bc.addServiceListener(listener);
118+
try {
119+
listener.doWaitForQuiet(quietPeriod, timeout);
120+
return listener.getEventCount();
121+
} finally {
122+
bc.removeBundleListener(listener);
123+
bc.removeServiceListener(listener);
124+
}
125+
}
126+
127+
/**
128+
* Registers for bundle events only, waits for quiet, then unregisters.
129+
*
130+
* @param bc the bundle context to register the listener on
131+
* @param quietPeriod required silence duration
132+
* @param timeout maximum total wait time
133+
* @return the number of events received during the wait
134+
* @throws InterruptedException if the waiting thread is interrupted
135+
* @throws QuiesceTimeoutException if {@code timeout} expires before the
136+
* quiet period is reached
137+
*/
138+
public static int waitForBundleQuiet(BundleContext bc, Duration quietPeriod, Duration timeout)
139+
throws InterruptedException, QuiesceTimeoutException {
140+
FrameworkQuiesce listener = new FrameworkQuiesce();
141+
bc.addBundleListener(listener);
142+
try {
143+
listener.doWaitForQuiet(quietPeriod, timeout);
144+
return listener.getEventCount();
145+
} finally {
146+
bc.removeBundleListener(listener);
147+
}
148+
}
149+
150+
/**
151+
* Registers for service events only, waits for quiet, then unregisters.
152+
*
153+
* @param bc the bundle context to register the listener on
154+
* @param quietPeriod required silence duration
155+
* @param timeout maximum total wait time
156+
* @return the number of events received during the wait
157+
* @throws InterruptedException if the waiting thread is interrupted
158+
* @throws QuiesceTimeoutException if {@code timeout} expires before the
159+
* quiet period is reached
160+
*/
161+
public static int waitForServiceQuiet(BundleContext bc, Duration quietPeriod, Duration timeout)
162+
throws InterruptedException, QuiesceTimeoutException {
163+
FrameworkQuiesce listener = new FrameworkQuiesce();
164+
bc.addServiceListener(listener);
165+
try {
166+
listener.doWaitForQuiet(quietPeriod, timeout);
167+
return listener.getEventCount();
168+
} finally {
169+
bc.removeServiceListener(listener);
170+
}
171+
}
172+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*******************************************************************************
2+
* Copyright (c) Contributors to the Eclipse Foundation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*******************************************************************************/
18+
19+
package org.osgi.test.common.quiesce;
20+
21+
import java.time.Duration;
22+
23+
/**
24+
* Thrown when the framework does not become quiet within the specified timeout.
25+
*/
26+
public class QuiesceTimeoutException extends RuntimeException {
27+
28+
private static final long serialVersionUID = 1L;
29+
30+
private final Duration quietPeriod;
31+
private final Duration timeout;
32+
private final int eventCount;
33+
34+
/**
35+
* @param quietPeriod the required silence duration that was not reached
36+
* @param timeout the maximum wait time that expired
37+
* @param eventCount the number of events received before timeout
38+
*/
39+
public QuiesceTimeoutException(Duration quietPeriod, Duration timeout, int eventCount) {
40+
super("Framework did not quiesce within " + timeout + " (required quiet period: " + quietPeriod + ", events: "
41+
+ eventCount + ")");
42+
this.quietPeriod = quietPeriod;
43+
this.timeout = timeout;
44+
this.eventCount = eventCount;
45+
}
46+
47+
/** Returns the required silence duration that was not reached. */
48+
public Duration getQuietPeriod() {
49+
return quietPeriod;
50+
}
51+
52+
/** Returns the maximum wait time that expired. */
53+
public Duration getTimeout() {
54+
return timeout;
55+
}
56+
57+
/** Returns the number of events received before timeout. */
58+
public int getEventCount() {
59+
return eventCount;
60+
}
61+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*******************************************************************************
2+
* Copyright (c) Contributors to the Eclipse Foundation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*******************************************************************************/
18+
19+
@org.osgi.annotation.bundle.Export
20+
@org.osgi.annotation.versioning.Version("1.0.0")
21+
package org.osgi.test.common.quiesce;

0 commit comments

Comments
 (0)