This is an automated email from the ASF dual-hosted git repository. ahuber pushed a commit to branch maintenance-branch in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 90b381d10d2bdba06706ef5b1cd11407f3f657c1 Author: andi-huber <[email protected]> AuthorDate: Wed Mar 4 16:40:27 2026 +0100 CAUSEWAY-3972: fixes _Oneshot deadlock potential --- .../causeway/commons/internal/base/_Oneshot.java | 39 ++++++--------- .../commons/internal/base/_OneshotTest.java | 56 ++++++++++++++++++++++ 2 files changed, 71 insertions(+), 24 deletions(-) diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/base/_Oneshot.java b/commons/src/main/java/org/apache/causeway/commons/internal/base/_Oneshot.java index 59c7c846397..ae1711a194e 100644 --- a/commons/src/main/java/org/apache/causeway/commons/internal/base/_Oneshot.java +++ b/commons/src/main/java/org/apache/causeway/commons/internal/base/_Oneshot.java @@ -19,35 +19,29 @@ package org.apache.causeway.commons.internal.base; import java.io.Serializable; +import java.util.concurrent.atomic.AtomicInteger; /** * <h1>- internal use only -</h1> - * <p> - * One-shot utility, thread-safe and serializable - * <p> - * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/> + * + * <p>One-shot utility, thread-safe and serializable + * + * <p><b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/> * These may be changed or removed without notice! + * * @since 2.0 */ public final class _Oneshot implements Serializable { private static final long serialVersionUID = 1L; - private final Object $lock = new Object[0]; // serializable lock - - private volatile int triggerCount = 0; + private final AtomicInteger counter = new AtomicInteger(0); // is serializable /** * Returns whether the trigger was accepted. */ public boolean trigger() { - synchronized ($lock) { - if(triggerCount==0) { - ++ triggerCount; - return true; - } - return false; - } + return counter.compareAndSet(0, 1); } /** @@ -56,23 +50,20 @@ public boolean trigger() { * If the {@link Runnable} throws an {@link Exception}, this one-shot will be exhausted regardless. */ public boolean trigger(final Runnable runnable) { - synchronized ($lock) { - if(triggerCount==0) { - ++ triggerCount; - runnable.run(); - } + // attempt to change 0 -> 1 atomically; only the thread that succeeds runs the runnable + if (counter.compareAndSet(0, 1)) { + runnable.run(); + return true; + } else return false; - } } /** - * resets to initial condition, that is it allows one more trigger + * resets to initial condition, that is, it allows one more trigger */ public void reset() { - synchronized ($lock) { - triggerCount = 0; - } + counter.set(0); } } diff --git a/commons/src/test/java/org/apache/causeway/commons/internal/base/_OneshotTest.java b/commons/src/test/java/org/apache/causeway/commons/internal/base/_OneshotTest.java new file mode 100644 index 00000000000..12ecfa93522 --- /dev/null +++ b/commons/src/test/java/org/apache/causeway/commons/internal/base/_OneshotTest.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.commons.internal.base; + +import java.util.concurrent.atomic.LongAdder; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +import lombok.SneakyThrows; + +class _OneshotTest { + + final _Oneshot a = new _Oneshot(); + final LongAdder counter = new LongAdder(); + + @Test + void test() { + a.trigger(this::sayHelloOnce); + } + + @SneakyThrows + void sayHelloOnce() { + if(counter.intValue()>0) { + fail("recursion detected"); + } + + counter.increment(); + + var thread = new Thread(this::doMoreWork); + thread.start(); + thread.join(); + } + + void doMoreWork() { + a.trigger(this::sayHelloOnce); + } + +}
