Add run-scoped shared resources#4
Merged
Merged
Conversation
Executable doc blocks re-run their side effects on every marklitGenerate (the block cache stores compiled classes, not output, so execution always replays). When docs talk to stateful external systems, a second generate in the same warm session sees the first run's leftovers. Previously the only workaround was a hand-rolled JVM-static + system-property bridge. Add a build-provided run resource: a class on the docs' classpath implementing java.util.function.Supplier[AutoCloseable]. marklit acquires it once before any file and closes it once after the last, inside a per-run ZIO.scoped boundary so teardown fires even on failure. The JDK seam means the resource class needs no dependency on marklit. While a resource is configured, a per-run user classloader holds the instance and parents every block's execution loader, so all blocks — including zio-app blocks — share the same instance (and the same ZIO). With no resource configured, behavior is byte-for-byte unchanged. - MarklitRunConfig.runResourceClass; acquireRunResource bracket in MarklitRun.runWith; CompilerFactory.userClassLoader builds the scoped per-run loader; ScalaCompiler.shareUserClasses parents block loaders off it. - Plumbed through the sbt plugin (marklitRunResourceClass), Mill (def marklitRunResourceClass), and CLI (--run-resource). - RunResourceSpec covers no-op baseline, exactly-once lifecycle, plain- and zio-app-block sharing, and setup/teardown failure-as-notice. New marklit/run-resource scripted test proves it end-to-end through the plugin. - README: Run-scoped resources section + CLI flag.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Executable doc blocks re-run their side effects on every
marklitGenerate— the block cache stores compiled classes, not output, so execution always replays. When docs talk to a stateful external system (a DB, a server, a temp dir), a second generate in the same warm sbt session sees the first run's leftovers and fails.This surfaced in the saferis docs: run 1 (21 files) all SUCCEED, run 2 has 4 files FAIL — those blocks do non-idempotent DB work (
createTablewithoutifNotExists, duplicate-key inserts) against a Postgres testcontainer that survives the whole session via a JVM system property. The old subprocess-per-generate model hid this; the warm in-process session exposes it. Until now the only fix was a hand-rolled JVM-static + system-property bridge.What
A first-class run-scoped resource lifecycle, build-provided and ZIO-bracketed:
java.util.function.Supplier[AutoCloseable].get()is setup (returns the teardown as theAutoCloseable). Using a JDK type lets the instance cross marklit's classloader boundary, so the user's class needs no dependency on any marklit artifact.ZIO.scoped+acquireReleaseinMarklitRun.runWith— teardown fires even on failure. Setup/teardown failures degrade to notices, never failing an otherwise-good run. The warmCompilerFactoryis untouched; only the resource is per-run.zio-appblocks — share the same instance (and the same ZIO). With no resource configured, behavior is byte-for-byte unchanged.Surface
MarklitRunConfig.runResourceClass;acquireRunResourcebracket;CompilerFactory.userClassLoader(scoped per-run loader);ScalaCompiler.shareUserClasses.marklitRunResourceClass), Mill (def marklitRunResourceClass), CLI (--run-resource).Note / tradeoff
When a run resource is configured,
zio-appblocks share the per-run ZIO instead of getting fresh per-block init — sharing is the point. With no resource configured, the per-block-fresh path is unchanged.Tests
RunResourceSpec(6 cases): no-op baseline unchanged; exactly-once acquire→close; plain-object sharing; zio-app sharing (no cross-loader LinkageError); setup-failure-as-notice; teardown-failure-as-notice.marklit/run-resourcescripted test: two generates in one sbt session — the second hitscache 100%yet still re-executes, and the lifecycle log is exactlyacquire,close,acquire,close.basicscripted test unchanged; full build incl. the Mill plugin compiles.Docs
README "Run-scoped resources" section + CLI flag table entry.
A saferis-side migration (add a
DocResource, setmarklitRunResourceClass, drop the system-property hack) is a separate change in that repo.🤖 Generated with Claude Code