Skip to content

v4.4.3

Latest

Choose a tag to compare

@github-actions github-actions released this 02 Aug 07:38
Immutable release. Only release title and notes can be modified.

✨ Features

  • Grain request scheduling (reentrancy) (#1290). Grains can now issue non-blocking requests and keep processing while a reply is in flight, extending the actor Request machinery to grains and closing the loop between the two process kinds: all four request edges (actor to actor, actor to grain, grain to grain, grain to actor) work locally and across nodes.
    • GrainContext.RequestGrain and GrainContext.RequestActor (and ReceiveContext.RequestGrain for actors) return a RequestCall handle: register a continuation with Then, or Cancel the request. Continuations, timeouts, and cancellations all run on the grain's own turn, so handler code stays single-threaded in every mode.
    • Reentrancy is enabled per grain with WithGrainReentrancy using the same reentrancy config actors use: AllowAll keeps processing any message between turns, StashNonReentrant pauses the user mailbox until the last in-flight request completes (buffered messages, timer ticks included, replay in exact arrival order), and MaxInFlight caps concurrent requests (ErrReentrancyInFlightLimit). Per-call WithRequestTimeout and WithReentrancyMode override the defaults; grain requests default to DefaultGrainRequestTimeout, and an explicit non-positive timeout disables it.
    • GrainContext.DeferResponse transfers reply ownership out of the current turn: it returns a one-shot GrainReply handle that a continuation or a later turn completes with Response, Err, or NoErr, which is what lets a grain answer an AskGrain only after its own downstream request comes back. CorrelationID exposes the request ID of the message being processed.
    • AskGrain against a reentrant grain no longer parks the caller on the grain's reply channels: the caller waits on a correlation slot while the grain keeps processing, so a deferred reply completes the ask without holding the grain's turn. Non-reentrant grains keep the existing ask path unchanged.
    • Reentrancy can also be enabled and disabled at runtime with EnableReentrancy / DisableReentrancy on both ReceiveContext and GrainContext, for capabilities needed only in particular cases rather than for the process's whole lifetime. Disabling only rejects new requests: in-flight requests carry the mode they were admitted with and complete, unpause, and unstash normally.
    • Passivation cooperates with in-flight requests: a pending request keeps the grain alive, and the idle-deactivation decision for reentrant grains is taken on the grain's own turn, so it cannot race message processing. Shutdown cancels in-flight requests with ErrRequestCanceled before deactivating.
    • The reentrancy config travels with the grain's wire record, so it survives eager relocation and remote activation. A grain reactivated by a bare send on a stored identity comes back with default config (no reentrancy) until activated with options again or re-enabled from a handler, and in-flight requests do not survive requester relocation or node crash: late replies to a fresh activation are dropped.
    • A TellGrain against a grain paused in StashNonReentrant mode can return ErrRequestTimeout even though the message is delivered and processes after resume; prefer RequestGrain toward reentrant grains. Internals are documented for maintainers in architecture/REENTRANCY.md.
  • Grain timers: activation-scoped timers for grains (#1288). Grains can now schedule messages to themselves with ScheduleOnce, Schedule, and ScheduleWithCron, available on both GrainContext (from OnReceive) and GrainProps (from OnActivate, the canonical place to start a grain's periodic behavior), plus CancelSchedule to stop a timer by reference. Ticks are delivered through the grain's mailbox and processed like any other message, so tick handling is serialized with the grain's other messages. Timers are volatile and scoped to the current activation: they are all cancelled when the grain deactivates, they are never persisted, and they never reactivate a passivated grain. Timers registered during OnActivate stay dormant until activation completes and are discarded when activation fails. By default a tick does not reset the grain's passivation clock; opt in per timer with WithTimerKeepAlive. References are scoped per grain and registering under a reference already in use replaces the existing timer (WithTimerReference sets one explicitly). Schedule fires at a fixed cadence like the actor scheduler, and cron expressions are evaluated in the process's local timezone with no cluster-wide arbitration needed, since a grain has exactly one activation cluster-wide. Steady-state ticks reuse the timer, the mailbox context, and the tick envelope, so recurring timers allocate almost nothing per fire.

🔧 Fixes

  • Reentrant Request and RequestName now reach actors on other nodes (#1290). An async request addressed to an actor on another node failed at the sender with no serializer found for message type *commands.AsyncRequest, so reentrancy only ever worked inside a single node. The request and response envelopes that carry the correlation ID and the reply address are now serialized and registered with the remoting layer, and a reply is delivered to the address recorded on the request rather than by resolving the requester's name, so replies work wherever remoting works and do not depend on the cluster registry. Deployment note for rolling upgrades: a node on this version can now put an async request on the wire, and a node on an older version has no serializer to decode it and discards it, so such a request only completes if it was given a WithRequestTimeout (there is no implicit default). Complete the rollout before relying on cross-node reentrancy.
  • memory.Free reported a quarter of the free memory on Apple Silicon. The macOS implementation ran the vm_stat command and scraped its output with two regexes. When the page-size line did not match it fell back to a hardcoded 4096-byte page size, but Apple Silicon uses 16 KiB pages, so any miss undercounted free memory fourfold. A miss on the free-pages line was worse: it returned zero bytes with a nil error, leaving a caller unable to tell an exhausted system from a failed read. The subprocess and both regexes are gone. Free now reads the vm.page_free_count and hw.pagesize sysctls and returns the underlying error when either read fails, so it can no longer report a plausible but wrong number. actor.Metric().MemoryAvailable() is the caller affected.
  • memory.Size truncated total RAM on OpenBSD and NetBSD. Those kernels type hw.physmem and hw.usermem as 32-bit integers and publish hw.physmem64 and hw.usermem64 for the real values, so a host with more than 4 GiB of RAM reported its size modulo 4 GiB, and a 32 GiB machine reported zero. Both accessors now prefer the 64-bit node when the platform publishes one and fall back to the classic name otherwise, which leaves FreeBSD and DragonFly unchanged since they already size the classic nodes as long. When neither node satisfies a 64-bit read the error is returned instead of a truncated value. The same change removes the package's hand-rolled sysctl decoder, which reinterpreted a []byte as a uint64 through an unaligned pointer after padding a NUL-trimmed string back to eight bytes; the reads now go through golang.org/x/sys/unix, which requests a fixed-size buffer and verifies the returned length.
  • Calling memory.Size or memory.Free broke the build on platforms without an implementation. Both were defined only for Linux, macOS, Windows and the BSDs, so any consumer that called them failed to compile for wasip1, js/wasm, solaris, aix or plan9 with undefined: memory.Size. Both now have a fallback on those platforms that returns errors.ErrUnsupported, so the limitation surfaces as a runtime error rather than a build failure in a downstream toolchain.

⚡ Performance

  • memory.Free no longer forks a process on macOS. Every call ran the vm_stat binary and parsed its output; it now issues two sysctls. actor.Metric calls Free on each invocation, so a metrics scrape no longer pays a fork and exec per sample. Resolving vm_stat through PATH is gone with it.

🔀 Pull Requests

Full Changelog: v4.4.2...v4.4.3