Skip to content

SnLib v1.27.0

Choose a tag to compare

@ValentinTarnovsky ValentinTarnovsky released this 11 Aug 22:05
· 2 commits to main since this release

A continuation a caller can chain onto

Nothing on SnFuture returned a new stage. thenSync, exceptionally and orDisablePlugin all end in return this, so a method ending in

return write(...).thenSync(publish);

whose caller writes callee().thenSync(next) did not build a chain. It registered two dependents on ONE CompletableFuture. Sibling order is unspecified and OpenJDK pops dependents last-registered-first, so next ran before publish, and read exactly the state the publish was about to install. Hopping to the main thread did not rescue it: both tasks were then queued in that same inverted order.

"B after A's continuation" was inexpressible on this surface, so no amount of documentation could fix it.

SnFuture.chainSync(Consumer<T>)

Runs the consumer on the main thread like thenSync, but returns a new future settled from inside that same task, after the consumer returned. Whatever the caller registers on it is a successor.

The fix belongs in the producer: swap its one thenSync and every existing caller becomes correct without being touched.

// before: the caller's step is a sibling of the publish
return writeState(KEY, date).thenSync(ignored -> lastResetDate = date);

// after: the caller's step is a successor
return writeState(KEY, date).chainSync(ignored -> lastResetDate = date);

Three deliberate divergences from thenSync

They all follow from the caller's handler now hanging off the derived future instead of the source.

  • A failure is propagated rather than swallowed into one WARN, so terminate the chain with exceptionally or orDisablePlugin. A chained failure nobody consumes is a failure nobody sees.
  • A consumer that throws fails the chain instead of only reaching Bukkit's task reporter, because a link that never settles is a permanent hang for whoever waits above it.
  • A skipped hop (plugin already disabled, context tearing down, or the disable race) completes the returned future normally with the value, so a teardown never turns into a hang. A normal completion therefore does not prove the consumer ran, and the two are deliberately indistinguishable.

Never wait on a chained future

While the plugin is running, only a main-thread task can complete it, so it carries the wrapMainCompleted marking and a main-thread join()/joinWithin throws instead of deadlocking the server. The marking is not applied when the plugin is already disabled or the context is already tearing down at the moment chainSync is called, because from then on the hop can never happen and the future settles on the completing thread; marking it anyway would make the guard fire during a teardown flush on a wait that would have returned.

A producer whose future a teardown flush joins must keep using thenSync. Joins belong on the future the database module returned.

One window does not settle and is documented rather than closed: a hop that was queued and then cancelled by a disable leaves the future pending. That is precisely what thenSync already did in the same window.

Also in this release

SnPapi.applyOnMain returned SnFuture.wrap for a future only its own main-thread task can complete. A future produced off the main thread and then waited on from it was a silent, permanent server deadlock with no log line. Both overloads now return wrapMainCompleted on the off-main branch, which turns the hang into an immediate throw. Shipped as its own commit, because it is a behaviour change rather than an addition.

Compatibility

Strictly additive. API level 17 -> 18. Every existing method is byte-for-byte unchanged, japicmp passes against the baseline, and every one of the 111 thenSync call sites across the consumer plugins keeps its exact current behaviour. An old consumer jar runs on 1.27.0 unchanged.

A consumer compiled against 1.27.0 inlines API level 18 into its bytecode and will refuse to enable against an older installed SnLib.jar, so update the library on the server before the plugin that needs it.

508 tests, 8 of them new, covering every branch of the sequencing core including the residual window above.