This is a history of changes to gateless/futurama
(async (doseq [c chans] (!<! c))), no longer grow the stack on each pass. async-read-port-take! used to unwrap nested async values with the public clojure.core.async/take!. That function's on-caller? argument defaults to true, so each resume ran the next step of the loop on the completing thread, on top of the previous frame. How this failed depended on where the stack ran out: a plain StackOverflowError, an error printed to stderr from a pool or manifold thread while the test still passed, or a hang. Callbacks now run on the caller only when their metadata says :on-caller?. Otherwise they are dispatched to a callback pool.async-read-port-take! now calls the ReadPort/take! protocol function instead of clojure.core.async/take!, so core.async's boxed fast-resume values reach it. It unwraps nested async values out of the box, and once it reaches a plain value it boxes that value again if the caller can accept a fast resume. futurama's own ReadPort implementations now return those boxed results from take!: AsyncReader (core.async channels), Future, IDeref, CompletableFuture, and manifold Deferred. When a value is already available, an async block can resume on the same thread without a race and without growing the stack.futurama.impl/delegating-handler wraps the committed callback and keeps its metadata, so :on-caller? carries through each level. If a value is read from the port, the handler is always committed. One caveat: mixing deeply nested async values with alts! is still an odd corner, and may not behave the way you expect.async-read-port-take! signature: The signature is now [x handler callback-pool fast-resume?]. Callers pass the pool their callbacks should run on, and say whether they can accept a boxed fast-resume return value. futurama.core passes a new private get-async-reader-pool, which is *thread-pool*, or (get-pool :mixed) when that is unset. futurama.impl is ^:no-doc, but this will break code that reached into it. The old async-reader-handler and async-reader-handler* helpers are gone.lots-of-channel-ops-in-a-loop-doesnt-blow-the-stack, which reads 10,000 async blocks one after another, each parked on a random timeout. This is the shape that overflowed the stack before this release.finally binding race: Fixed a race where bindings established inside an async/go block could still be lost across a park, even with 1.4.8's snapshotting terminators. core.async's generated state machine writes the current thread binding frame back into the shared state array in a finally on every exit from a run. When a park resumes on another pool thread before the parking thread unwinds, that finally overwrites BINDINGS-IDX with a stale frame, discarding whatever the resumed run established. futurama now patches clojure.core.async.impl.go/emit-state-machine to omit the finally write entirely; the binding frame is captured only by the parking terminators, before the resume callback is registered. Both halves are required — 1.4.8 shipped only the terminators.futurama.core-async-patching (^:no-doc) now owns every core.async monkey-patch — the emit-state-machine replacement and the ioc-take!/ioc-put!/ioc-alts! terminators, relocated from futurama.impl. One namespace, one responsibility: everything futurama alters in core.async is visible in a single file.go or async blocks that are compiled before futurama.core loads keep the unpatched state machine, silently and permanently for that JVM. Load futurama.core as one of the first namespaces at application start — see Load Order. This also applies at build time for AOT/uberjar/native-image compilation.binding-nested-inside-async-and-go-block-survives-park, covering nested binding forms with parks interspersed in both async and plain go blocks (500 iterations each, park window deterministically widened). Retargeted the existing binding-race tests at futurama.core-async-patching/ioc-take!.binding are now preserved across parking operations (<!, >!, alts! and the futurama !<! equivalents), even when a parked async/go block resumes on a different pool thread. Previously, a resume that fired on another thread before the parking thread finished saving its binding frame could observe a stale frame and lose bindings set inside the block. This is fixed by custom IOC "parking terminators" (futurama.impl/ioc-take!, ioc-put!, ioc-alts!) that snapshot the current thread binding frame before registering the resume callback. Validated against core.async 1.8 and 1.9.go coverage: async! uses the snapshotting terminators directly. Because plain core.async go blocks read their terminators from a hardcoded var at macroexpansion time (no injection point), futurama also alters the root of clojure.core.async.impl.ioc-macros/async-custom-terminators so go blocks compiled after the library loads get the same guarantee. This is a global, JVM-wide effect and a temporary measure pending an upstream core.async fix.ReadPort/take! logic into futurama.impl/async-read-port-take!, adding a poll! synchronous fast-path for ready core.async channels and committing the read handler up front. The up-front commit fixes a bug where reading a plain (non-async) value through an ->async-reader inside alts! could leave a phantom taker on the losing port that later consumed and dropped a value.async/go, verified over thousands of iterations with the park window deterministically widened) and ->async-reader/alts! read-port coverage. Replaced the bond get-pool spy — which was subject to cross-thread pollution — with pure, deterministic macroexpansion assertions, and replaced criterium benchmarks with a lightweight min-elapsed-ms helper.dev.weavejester/cljfmt 0.16.5 (from cljfmt/cljfmt 0.9.2), consolidated indentation config into a single .cljfmt.edn (unqualified symbol keys, so clojure-lsp and the CLI stay aligned), and removed the now-unused circleci/bond test dependency. Added an antq dependency-report tool (:deps-antq alias) with make deps-check / make deps-upgrade targets that exclude org.clojure/core.async (which is version-matrixed by hand), and bumped the CI actions (actions/checkout, jdx/mise-action).clj-kondo (2026.07.24), test.check (1.1.3), slf4j-simple (2.0.18), nrepl (1.7.0), cider-nrepl (0.62.2), graal-build-time (1.0.6), and deps-deploy (0.2.5). Dropped pjstadig/humane-test-output (no longer recommended for use) along with its activation in the test setup.satisfies? protocol checks behind inlining predicate macros in futurama.impl (async?, async-channel?, async-completable-reader?, async-completable-writer?, async-cancellable?), and switched futurama.core/futurama.impl call sites over to them. The public futurama.core/async? remains a function so it can still be passed as a higher-order value.!<! and !<!! now short-circuit non-async values, returning them directly without a channel round-trip; the argument expression is still evaluated exactly once.!<!/!<!! non-async fast path, including the single-evaluation guarantee for both non-async and async argument expressions.get-pool once again returns an ExecutorService (1.4.5 had narrowed the return type to Executor to track core.async 1.9's executor-for, which broke downstream consumers that called .submit/.invokeAll on the pool). When executor-for returns a plain Executor, the result is widened via a new futurama.impl/->executor-service proxy that forwards execute; real ExecutorService instances pass through unwrapped. The internal async-dispatch-task-handler continues to accept any Executor.UnsupportedOperationException from shutdown, shutdownNow, isShutdown, isTerminated, and awaitTermination — it does not own the underlying executor and refuses to lie about its lifecycle. Callers that need to manage a pool's lifecycle should hold a reference to the original Executor directly.->executor-service (passthrough vs. wrap, execute/submit routing, lifecycle behavior on both branches) and for the previously-untested with-async-factory and with-thread-factory macros (binding/restore, precedence, nesting, exception unwind).async-dispatch-task-handler now accepts any java.util.concurrent.Executor (previously required ExecutorService). Tasks are wrapped in a FutureTask so cancel-with-interrupt and exception-capture semantics are preserved. Required to support core.async 1.9's clojure.core.async.impl.dispatch/executor-for, which returns a plain Executor.get-pool now falls back to clojure.core.async.impl.dispatch/executor-for (was ForkJoinPool/commonPool). Out of the box, futurama dispatches over the same workload-aware pools as core.async — no futurama.executor-factory sysprop required for that behavior. The sysprop remains the override hook..mise.toml JDK to corretto-25 (latest LTS); updated .mise.toml to the new [tool_alias] schema; default TEST_CORE_ASYNC_ALIAS flipped to core.async-1.9 in the Makefile.->future function to easily convert values to future.1.8.735completable-future and fixed-threadpool1.8.730async macro, replace uses with async.thread, updated async, both macros route work to the appropriate thread pool, such as :io, :compute, or :mixed.*thread-factory* dynamic binding to allow separately defining a factory-fn for thread calls, distinct from async calls.completable-future macro, uses of completable-future should be replaced with thread.fixed-threadpool function, instead prefer to use Executors thread pools according to need.async-cancellable? fn to easily test if something can be cancelledasync-future and async-deferred macros to more easily create either.cancel! and cancelled? to async-cancel! and async-cancelled?fixed-threadpool method to create a FixedThreadPool*thread-pool* is now a FixedThreadPool which can be interrupted.async to be interrupted just like completable-future, add tests.with-pool macroFuture and IDeref.satisfies? to instance-satisfies? and class-satisfies?async-reduce, async-some, async-every?, async-walk/prewalk/postwalkasync-for so it uses less async macros and it is more flexibleasync-map so it leverages async-for behind the scenes.async-some and async-every? and instead added some new helpers.async-> and async->> threading macros to make it easier to thread async.async-map, async-some, async-every?async-for by just executing each iteration inside an async block and then collect afterasync-for comprehension which implicitly runs inside an async blockasync? helper function which is useful!<! and !<!!Can you improve this documentation? These fine people already did:
Jose Gomez & Kevin DowneyEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |