Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.softwaremill.jox.structured;

import java.util.concurrent.Callable;

public interface Decorator {
<T> Callable<T> decorate(Callable<T> f);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,30 @@ public final class Scope {
private final Lock externalSchedulerLock = new ReentrantLock();
private volatile ActorRef<ExternalScheduler> externalSchedulerActor;

/**
* Decorates the body of every fork started in a scope, with the decorator applied on the thread
* which calls {@link #fork} (and its variants), before the fork is started.
*
* <p>To set the decorator, use the {@link ScopedValue#where(ScopedValue, Object)} method, e.g.
* {@code ScopedValue.where(Scope.FORK_DECORATOR, myDecorator).run(() -> supervised(...))}. It
* is then used by all the scopes which are created within that binding.
*/
public static final ScopedValue<Decorator> FORK_DECORATOR = ScopedValue.newInstance();

Scope() {
this.scopeDone = new AtomicBoolean(false);
this.rawScope = StructuredTaskScope.open(new CancelWhenDoneJoiner(scopeDone));
this.supervisor = new Supervisor();
}

/**
* Applies the {@link #FORK_DECORATOR}, if any, to the body of a fork. Called on the thread
* which starts the fork, so that the decorator can capture that thread's state.
*/
private static <T> Callable<T> decorate(Callable<T> f) {
return FORK_DECORATOR.isBound() ? FORK_DECORATOR.get().decorate(f) : f;
}

Supervisor getSupervisor() {
return supervisor;
}
Expand Down Expand Up @@ -96,13 +114,14 @@ void cancelAndJoinRawScope() throws InterruptedException {
*/
public <T> Fork<T> fork(Callable<T> f) throws InterruptedException {
var result = new ForkUsingResult<T>();
var decorated = decorate(f);
supervisor
.getCommands()
.send(
(RunFork<T>)
() -> {
try {
result.complete(f.call());
result.complete(decorated.call());
} catch (Throwable e) {
// we notify the supervisor first, so that if this is the
// first failing fork in the scope, the supervisor will
Expand Down Expand Up @@ -133,14 +152,15 @@ public <T> Fork<T> fork(Callable<T> f) throws InterruptedException {
*/
public <T> Fork<T> forkUser(Callable<T> f) throws InterruptedException {
var result = new ForkUsingResult<T>();
var decorated = decorate(f);
supervisor.forkUserStarts();
supervisor
.getCommands()
.send(
(RunFork<T>)
() -> {
try {
result.complete(f.call());
result.complete(decorated.call());
supervisor.forkUserSuccess();
} catch (Throwable e) {
if (!supervisor.forkException(e)) {
Expand All @@ -166,13 +186,14 @@ public <T> Fork<T> forkUser(Callable<T> f) throws InterruptedException {
*/
public <T> Fork<T> forkUnsupervised(Callable<T> f) throws InterruptedException {
var result = new ForkUsingResult<T>();
var decorated = decorate(f);
supervisor
.getCommands()
.send(
(RunFork<T>)
() -> {
try {
result.complete(f.call());
result.complete(decorated.call());
} catch (Throwable e) {
result.completeExceptionally(e);
}
Expand Down Expand Up @@ -204,6 +225,7 @@ public <T> CancellableFork<T> forkCancellable(Callable<T> f) throws InterruptedE
// forks can be never run, if they are cancelled immediately - we need to detect this, not
// to await on result.get()
var result = new CancellableForkUsingResult<T>();
var decorated = decorate(f);
supervisor
.getCommands()
.send(
Expand All @@ -213,7 +235,9 @@ public <T> CancellableFork<T> forkCancellable(Callable<T> f) throws InterruptedE
.run(
nestedScope ->
forkCancellableNestedScope(
nestedScope, result, f));
nestedScope,
result,
decorated));
return null;
});
return result;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package com.softwaremill.jox.structured;

import static com.softwaremill.jox.structured.Scopes.supervised;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;

import org.junit.jupiter.api.Test;

public class ForkDecoratorTest {

/** Carries the value bound when a fork is started into the fork's body. */
private static class PropagatingDecorator implements Decorator {
@Override
public <T> Callable<T> decorate(Callable<T> f) {
var captured = VALUE.isBound() ? VALUE.get() : null;
return () ->
captured == null ? f.call() : ScopedValue.where(VALUE, captured).call(f::call);
}
}

private static final ScopedValue<String> VALUE = ScopedValue.newInstance();

private static String valueInFork() {
return VALUE.isBound() ? VALUE.get() : null;
}

@Test
void shouldNotSeeAValueBoundInTheScopeBodyWithoutADecorator() throws Exception {
var seen =
supervised(
scope ->
ScopedValue.where(VALUE, "bound-in-body")
.call(
() ->
scope.fork(ForkDecoratorTest::valueInFork)
.join()));

// forks are started by the thread running `supervised`, which is outside the binding
assertNull(seen);
}

@Test
void shouldSeeAValueBoundInTheScopeBodyWithADecorator() throws Exception {
var seen =
ScopedValue.where(Scope.FORK_DECORATOR, new PropagatingDecorator())
.call(
() ->
supervised(
scope ->
ScopedValue.where(VALUE, "bound-in-body")
.call(
() ->
scope.fork(
ForkDecoratorTest
::valueInFork)
.join())));

assertEquals("bound-in-body", seen);
}

@Test
void shouldDecorateEachForkWithTheValueBoundWhenItWasStarted() throws Exception {
var seen =
ScopedValue.where(Scope.FORK_DECORATOR, new PropagatingDecorator())
.call(
() ->
supervised(
scope -> {
List<String> results =
new CopyOnWriteArrayList<>();
for (var v : List.of("first", "second")) {
ScopedValue.where(VALUE, v)
.call(
() ->
results.add(
scope.fork(
ForkDecoratorTest
::valueInFork)
.join()));
}
return results;
}));

// each fork carries the value bound at its own start, not one value for the whole scope
assertEquals(List.of("first", "second"), seen);
}

@Test
void shouldDecorateForksOfNestedScopes() throws Exception {
var seen =
ScopedValue.where(Scope.FORK_DECORATOR, new PropagatingDecorator())
.call(
() ->
supervised(
outer ->
ScopedValue.where(VALUE, "bound-in-body")
.call(
() ->
outer.forkUser(
() ->
supervised(
inner ->
inner.forkUser(
ForkDecoratorTest
::valueInFork)
.join()))
.join())));

assertEquals("bound-in-body", seen);
}

@Test
void shouldDecorateAllForkVariants() throws Exception {
var decorated = new CopyOnWriteArrayList<String>();
Decorator counting =
new Decorator() {
@Override
public <T> Callable<T> decorate(Callable<T> f) {
decorated.add("decorated");
return f;
}
};

ScopedValue.where(Scope.FORK_DECORATOR, counting)
.call(
() ->
supervised(
scope -> {
scope.fork(() -> 1).join();
scope.forkUser(() -> 1).join();
scope.forkUnsupervised(() -> 1).join();
scope.forkCancellable(() -> 1).join();
return null;
}));

// the scope's main body fork, plus the four above, plus the body of the nested scope
// which forkCancellable starts internally
assertEquals(6, decorated.size());
}
}
Loading