Scope locals

Ludovic Henry ludovic.henry at datadoghq.com
Thu Jun 10 07:43:14 UTC 2021


Hello,

I’ve been playing with this ScopeLocal proposal and it does fit a whole set
of needs, so thank you for that. The only pain point has been around
integrating it to existing patterns, and more specifically the TWR (try
with resources) pattern.

The current API of ScopeLocal forces the user to use
`ScopeLocal.Carrier.run` and `ScopeLocal.Carrier.call` to set the ambient
context. I understand that this approach is a great match for Structured
Concurrency which Loom is promoting. However, let’s take the following
example of a Transaction class which represents an overall transaction in a
system and that holds some sort of state.

```
class Transaction implements AutoCloseable {
 public Transaction();
 public static Transaction current();
 public void close();
}
```

A user would use it like so:
```
try (final Transaction transaction = new Transaction()) {
 // do something like call a database, read a file. During all these
 // operations, anywhere the user can call Transaction.current() and
 // get this `transaction` object.
 doSomething();
}
```

Today, this Transaction class uses, let’s say, ThreadLocal to store the
currently executing transaction on this thread.

```
class Transaction implements AutoCloseable {
 private static final ThreadLocal<Transaction> currentTransaction =
   ThreadLocal<Transaction>.withInitial(() -> null);
 public Transaction() {
  currentTransaction.set(this);
 }
 public static Transaction current() {
  return currentTransaction.get();
 }
 public void close() {
  currentTransaction.remove();
 }
}
```

Now, if we want to use a ScopeLocal to store the current transaction, the
API of `Transaction` would have to change by requiring the user to provide
a Runnable or Callable to pass to `ScopeLocal.Carrier.run` or
`ScopeLocal.Carrier.call` respectively. The Transaction class would then
need to change to something like so:

```
class Transaction implements AutoCloseable {
 private static final ScopeLocal<Transaction> currentTransaction =
ScopeLocal.forType(Transaction.class);
 public Transaction() {
  // no-op
 }
 // New method to match ScopeLocal requirements
 public void run(Runnable runnable) {
  ScopeLocal.where(currentTransaction, this).run(runnable);
 }
 public static Transaction current() {
  return currentTransaction.get();
 }
 public void close() {
  // no-op
 }
}
```

The user of Transaction would then switch to the following code:
```
try (final Transaction transaction = new Transaction()) {
 // here, Transaction.current() _does not_ return this `transaction` object.
 transaction.run(() -> {
  // and here, Transaction.current() _does_ return this `transaction`
object.
  doSomething();
 });
}
```

I agree that this is not a huge code change in itself, but it is a breaking
change of the API of this library. Scaling this change to much larger
libraries would have ripple effects which makes using ScopeLocal in such
cases likely impossible.

The main alternative I’m seeing is for ScopeLocal to provide a TWR-like API
in complement to the Runnable/Callable-based API. Something that could be
used like the following:

```
class Transaction implements AutoCloseable {
 private static final ScopeLocal<Transaction> currentTransaction =
ScopeLocal.forType(Transaction.class);
 private final ScopeLocal.Context currentContext;
 public Transaction() {
   currentContext = ScopeLocal.where(currentTransaction,
this).buildContext();
 }
 public static Transaction current() {
  return currentTransaction.get();
 }
 public void close() {
  currentContext.close();
 }
}
```

Thank you,
Ludovic


More information about the loom-dev mailing list