Skip to content

Instantly share code, notes, and snippets.

@MBoegers
Last active March 13, 2025 09:41
Show Gist options
  • Save MBoegers/87fa0928c46e037155dbc7d48aaf7553 to your computer and use it in GitHub Desktop.
Save MBoegers/87fa0928c46e037155dbc7d48aaf7553 to your computer and use it in GitHub Desktop.
RuleEngine with Modern Java Concurrency
import javax.net.ssl.SSLContext;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.List;
import java.util.Map;
/// This engine uses [JEP 481](https://openjdk.org/jeps/481) {@link ScopedValue} technique to forward execution context thread save.
class Engine {
private final SSLContext sslContext;
Engine(SSLContext sslContext) {
this.sslContext = sslContext;
}
public static abstract class Rule {
/// Models the Adress of an attendee.
/// The `record` implementation ensures that the data is immutable.
record Address(String city, String street, int house) {
}
/// Read the address from the **context** as via the scoped value API and generates the `Address` PoJo
protected Address readAddress() throws InterruptedException {
String city = CONTEXT.get().get("city");
String street = CONTEXT.get().get("street");
int house = Integer.parseInt(CONTEXT.get().get("house"));
Thread.sleep(Duration.ofSeconds(1)); // simulate a time consuming operation
return new Address(city, street, house);
}
protected String readName() {
return CONTEXT.get().getOrDefault("name", "UNKNOWN");
}
/// Method to implement by the business rules.
/// The implementations can Thread-save leverage the `Rule.read*` methods.
abstract void fire();
}
private final static ScopedValue<Map<String, String>> CONTEXT = ScopedValue.newInstance();
private final static ScopedValue<SSLContext> SSL_CTX = ScopedValue.newInstance();
void execute(List<Rule> rules, Map<String, String> data) {
ScopedValue.Carrier executionScope = ScopedValue
.where(CONTEXT, data)
.where(SSL_CTX, sslContext);
for (var r : rules) {
executionScope.run(r::fire);
}
}
}
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
void main() throws NoSuchAlgorithmException {
var merlin = Map.of(
"name", "Merlin",
"city", "Herne",
"street", "Mont-Cenis-Str.",
"house", "294");
var falk = Map.of(
"name", "Falk",
"city", "Darmstadt",
"street", "Donno",
"house", "12");
var engine = new Engine(SSLContext.getDefault());
var rules = List.<Engine.Rule>of(new PrintAttendeeInfos());
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10; i++) {
executor.submit(() -> engine.execute(rules, merlin));
executor.submit(() -> engine.execute(rules, falk));
}
}
System.out.println("Execution finished");
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.StructuredTaskScope;
import java.util.function.Supplier;
/// This Rule leverages the [JEP 480](https://openjdk.org/jeps/480) StructuredConcurrency API to obtain data for the logic
class PrintAttendeeInfos extends Engine.Rule{
@Override
void fire() {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> name = scope.fork(() -> {
System.out.println("\tRead Name");
var tmp = readName();
System.out.println("\tGot Name");
return tmp;
});
Supplier<Address> adress = scope.fork(() ->{
System.out.println("\tRead Address");
var tmp = readAddress();
System.out.println("\tGot Address");
return tmp;
});
scope.join().throwIfFailed(); // Wait for both forks
var addr = adress.get();
System.out.printf("%s's home is %s %d, %s%n", name.get(), addr.street(), addr.house(), addr.city());
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment