movie-cr

Movie

Movie is a lightweight typed actor framework for Crystal. It provides actor lifecycle and supervision, ask/futures, scheduling, bounded execution, pluggable SQLite/PostgreSQL persistence, typed streams, restart-tolerant TCP remoting associations, static-seed cluster membership, sharding, cluster singletons, typed service discovery, and cluster-aware routers.

Guides · Crystal API reference

Feature maturity

Area Status Notes
Typed actors and lifecycle Stable core Hierarchical actors, mailbox isolation, watching, restart/stop/resume supervision, and orderly shutdown.
Futures, ask, scheduler Stable public API Thread-safe terminal futures, lightweight local ask response refs, and cancellable one-shot timers.
Executor Advanced API Bounded worker pool; task timeout does not cancel the task body.
Persistence Production beta Versioned schemas, typed effects, atomic revisions, recovery, safe retention, projections, transactional outbox, telemetry/resilience, SQLite, shared PostgreSQL, and PostgreSQL-fenced sharded entities.
Typed streams MVP Manual sources, transform stages, fold/collect sinks, cancellation, backpressure, and broadcast fan-out.
Remoting Production beta Versioned/authenticated associations, bounded reconnect, heartbeat failure detection, reliable control traffic, and at-most-once user delivery.
Cluster membership Production alpha Static seeds, UID-safe process incarnations, convergent gossip, deterministic leadership, reachability, graceful leave, events, and manual downing.
Cluster sharding Production alpha Logical entity refs, pluggable partition/allocation/rebalance strategies, activation/passivation, relocation, and PostgreSQL lease fencing; no automatic split-brain resolution.
Cluster singleton Production alpha Eager role-aware ownership, stable typed proxies, graceful handoff, real-process failure coverage, and PostgreSQL-fenced persistent variants.
Cluster receptionist Production alpha Typed service keys, UID-safe replicated listings, lifecycle cleanup, subscriptions, and real-process failure coverage.
Cluster routers Production alpha Dynamic typed group routers, four routing strategies, role/locality filtering, explicit per-node pools, and real-process failure coverage.

Requirements and installation

  • Crystal 1.19.1 through 1.21.x.
  • SQLite development headers when using persistence or running the full test suite.

Add Movie to shard.yml:

dependencies:
  movie:
    github: mikeoz32/movie

Then install dependencies:

shards install

Typed actors

The main entrypoint includes the actor runtime, async primitives, streams, and remoting:

require "movie"

class Printer < Movie::AbstractBehavior(String)
  def initialize(@received : Channel(String))
  end

  def receive(message : String, context : Movie::ActorContext(String))
    puts message
    @received.send(message)
    Movie::Behaviors(String).same
  end
end

received = Channel(String).new(1)
system = Movie::ActorSystem(Nil).new(Movie::Behaviors(Nil).same, name: "example")
printer = system.spawn(Printer.new(received), name: "printer")
printer << "hello from Movie"
received.receive
system.shutdown

An actor returns its next behavior from receive. Behaviors(T).same keeps the active behavior, Behaviors(T).stopped requests a graceful stop, and Behaviors(T).setup builds a behavior with access to its ActorContext.

A parent's SupervisionConfig controls failures of that parent's children. See the lifecycle architecture and the corrected supervision example.

Ask, futures, scheduler, and executor

ActorRef#ask, ActorContext#ask, and ActorSystem#ask are the local request/response APIs. They return Future(T), which completes once with a value, exception, or cancellation. Future#await raises Movie::FutureTimeout for a waiting timeout, Movie::FutureCancelled for cancellation, and re-raises the original failure.

Actors reply with Movie::Ask.reply_if_asked(context.sender, value) or the explicit success/failure helpers.

Movie::Scheduler provides schedule_once, schedule_message, and schedule_system_message. Cancelling a TimerHandle prevents a callback only if it has not fired; it does not interrupt running work.

Movie::Execution.get(system) exposes the bounded executor. execute returns a future; execute_with_reply sends TaskSuccess(T) or TaskFailure(T). A timeout completes the result path with FutureTimeout but does not cancel the underlying task body.

Persistence

Persistence is intentionally optional and has a separate entrypoint:

require "movie"
require "movie/persistence"

config = Movie::Config.builder
  .set("persistence.db-path", "data/movie.sqlite3")
  .set("persistence.pool-size", 1)
  .set("persistence.io-queue-capacity", 256)
  .set_duration("persistence.operation-timeout", 5.seconds)
  .build

system = Movie::ActorSystem(Nil).new(Movie::Behaviors(Nil).same, config)
event_sourcing = Movie::EventSourcing.get(system)
durable_state = Movie::DurableState.get(system)

EventSourcedBehavior and DurableStateBehavior use typed effects, optimistic revisions, restart-safe recovery, and post-persist callbacks. Event batches and outbox messages are atomic; snapshots enable safe journal retention; global event offsets and durable checkpoints support projections. Backend connections run on dedicated isolated connection threads with bounded retries, circuit breaking, metrics, and active readiness probes. PostgreSQL-backed entities can use cluster sharding or cluster singleton for fenced single-writer relocation. See the persistence guide for the complete API and cluster limits.

Run the complete event-sourcing example with crystal run examples/persistence_example.cr -Dpreview_mt -Dexecution_context.

Run the query, projection, outbox, and telemetry example with crystal run examples/persistence_operations_example.cr -Dpreview_mt -Dexecution_context.

For a shared PostgreSQL journal, require movie/persistence/postgres, set persistence.backend = postgres and persistence.connection-uri, or run MOVIE_POSTGRES_URL=postgres://... crystal run examples/postgres_persistence_example.cr -Dpreview_mt -Dexecution_context.

Typed streams

Typed streams run on an existing actor system and do not create a hidden runtime:

alias Streams = Movie::Streams::Typed
alias Message = Streams::MessageBase(Int32)

system = Movie::ActorSystem(Message).new(Movie::Behaviors(Message).same)
pipeline = Streams.manual(Int32)
  .via(Streams::MapFlow(Int32).new { |value| value * 2 })
  .to_collect(initial_demand: 2u64, channel_capacity: 2)
  .run(system)

pipeline.source << Streams::Produce(Int32).new(1)
pipeline.source << Streams::Produce(Int32).new(2)
pipeline.source << Streams::OnComplete(Int32).new
pipeline.completion.await
system.shutdown

See the streams protocol, typed blueprint example, legacy basic example, and showcase.

Remoting

Remoting provides typed delivery over versioned, restart-tolerant associations. Wire messages must include JSON::Serializable and be registered with Movie::Remote::MessageRegistry on both systems.

It supports typed fire-and-forget delivery, remote ask, sender paths, remote Stop, Watch, Unwatch, Terminated, and Failed, automatic reconnect, HMAC handshake authentication, heartbeat failure detection, and acknowledged/deduplicated outbound control messages. User traffic remains at-most-once and is not replayed after disconnect; use the persistence outbox for durable business delivery. TLS is installed through transport wrapping hooks because certificate ownership belongs to the application deployment.

ActorSystem#actor_for returns ActorRefBase; narrow a remote result before using its typed API:

remote = system.actor_for(remote_path, Ping).as(Movie::Remote::RemoteActorRef(Ping))
remote << Ping.new(1)
reply = remote.ask(Request.new("hello"), Response).await(2.seconds)

See the remoting contract and complete example.

Cluster membership

Cluster membership runs through a typed daemon actor over the existing remoting associations. A seed can form a one-node cluster; other nodes retry static seeds until they join:

seed_remote = seed_system.enable_remoting("127.0.0.1", 2551)
seed = seed_system.enable_cluster(Movie::Cluster::ClusterSettings.new(roles: ["seed"]))

worker_system.enable_remoting("127.0.0.1", 2552)
worker = worker_system.enable_cluster(Movie::Cluster::ClusterSettings.new(
  seed_nodes: [seed_remote.address],
  roles: ["worker"]
))
worker.await_up

Reachability never removes a member automatically. Resolve the partition externally, then call down on the current leader for the exact non-local UniqueAddress; remote down requests additionally require the remoting shared secret. Use that secret for every non-isolated deployment. Graceful shutdown calls leave, waits with await_removed, and only then stops the actor system. See the cluster guide and complete example.

Cluster sharding

Movie::ClusterSharding is a separate extension above membership. It maps stable entity ids to shards, places those shards with a selectable least-loaded, rendezvous, weighted, and/or role-aware strategy, and advances placement through a separate no-rebalance or rate-limited rebalance policy. Logical refs support typed tell, ask, explicit passivation, idle passivation, and relocation without exposing physical actor paths.

Event-sourced and durable-state entities can use the same surface with PostgreSQL leases and transactionally validated fencing epochs. During ambiguous partitions persistent sharding fails closed; reachability alone never grants ownership. See the sharding guide and complete example.

Cluster singleton

Movie::ClusterSingleton composes above sharding and keeps one logical actor eagerly active on one eligible node. Every node receives the same stable typed proxy; optional role filters constrain ownership without constraining callers. A graceful owner leave drains accepted work before activating the replacement, and explicit Stop recreates the actor behind the same proxy.

Event-sourced and durable-state singleton helpers require PostgreSQL and reuse the same transactionally validated fencing epochs as persistent sharding. Reachability alone never moves ownership, and Movie still requires an external split-brain decision before explicit downing. See the singleton guide and complete example.

Cluster receptionist

Movie::ClusterReceptionist provides typed discovery for zero or more local or remote actor services. Registrations are ephemeral, watched for actor termination, replicated as authenticated per-node revisioned state, filtered by current membership reachability, and exposed through snapshots or typed listing subscriptions. It does not deploy actors, choose an owner, or change membership. See the receptionist guide and two-node example.

Cluster routers

Movie::ClusterRouters follows receptionist service keys and exposes one typed ref with round-robin, random, broadcast, or deterministic rendezvous-hash selection. Optional role filtering and local preference operate on the current reachable listing. Explicit pools create and own routees only on the node where they are configured; Movie does not deploy actors remotely. Empty routes fail immediately and router delivery never adds buffering, retries, or replay. See the cluster router guide and two-node pool example.

Configuration

Configuration supports YAML, JSON, builders, fallbacks, and environment overrides. Public keys use dotted sections and hyphenated compound names, for example supervision.max-restarts and remoting.stripe-count.

The complete schema, null semantics, error behavior, defaults, and environment-variable mapping are documented in the configuration guide.

API stability

Stable application-facing APIs:

  • typed actors, actor references, lifecycle, supervision, and shutdown;
  • local ask APIs and Future(T) read-side operations;
  • scheduler one-shot timers and TimerHandle.

Advanced APIs that may change more aggressively:

  • Promise(T) callback bridging;
  • executor protocol types and direct executor integrations;
  • persistence entity/store internals;
  • streams while they remain an MVP feature, remoting association internals while the transport remains beta, and cluster membership, sharding, singleton, receptionist, and router APIs while they remain production alpha.

Development and verification

Every implementation task follows the repository workflow: write and observe a failing test before production code, run fresh targeted and broad verification, update public documentation, and complete a review pass. Contributors can read the internal development workflow in the repository.

Default correctness gates:

crystal tool format --check src spec examples
crystal spec spec/movie -Dpreview_mt -Dexecution_context
for file in examples/*.cr; do crystal build "$file" -Dpreview_mt -Dexecution_context -o "/tmp/movie-$(basename "$file" .cr)"; done

Benchmarks and stress scenarios are intentionally opt-in:

MOVIE_BENCH=1 crystal spec --release spec/movie/remote/benchmark_spec.cr -Dpreview_mt -Dexecution_context
MOVIE_BENCH=1 crystal spec --release spec/movie/remote/association_benchmark_spec.cr -Dpreview_mt -Dexecution_context
MOVIE_STRESS=1 crystal spec spec/movie/remote/stress_spec.cr -Dpreview_mt -Dexecution_context
MOVIE_CLUSTER_STRESS=1 crystal spec spec/movie/cluster/stress_spec.cr -Dpreview_mt -Dexecution_context
MOVIE_SINGLETON_STRESS=1 crystal spec spec/movie/cluster/singleton_stress_spec.cr -Dpreview_mt -Dexecution_context
MOVIE_RECEPTIONIST_STRESS=1 crystal spec spec/movie/cluster/receptionist_stress_spec.cr -Dpreview_mt -Dexecution_context
MOVIE_ROUTER_STRESS=1 crystal spec spec/movie/cluster/router_stress_spec.cr -Dpreview_mt -Dexecution_context
MOVIE_CLUSTER_BENCH=1 crystal spec --release spec/movie/cluster/benchmark_spec.cr -Dpreview_mt -Dexecution_context

Benchmark output is measurement-only because absolute throughput and relative speedup depend on the host, Crystal version, and scheduler. Correctness remains enforced by the default and stress suites.

ActorSystem end-to-end benchmark

The standalone ActorSystem runner compares the same serializable tell and ask workloads across local delivery, two actor systems connected in-process, and two separate processes over TCP loopback. Tell batches stop timing only after actor-side snapshot barriers confirm that every message was processed; remote results therefore include serialization, framing, TCP, routing, mailbox dispatch, and behavior execution rather than only socket writes.

Build the runner in release mode:

crystal build benchmarks/actor_system.cr --release -Dpreview_mt -Dexecution_context \
  -o /tmp/movie-actor-system-benchmark

Run a comparable topology matrix:

/tmp/movie-actor-system-benchmark \
  --topology all \
  --operation both \
  --messages 100000 \
  --payload-bytes 64 \
  --producers 8 \
  --actors 8 \
  --in-flight 64 \
  --stripes 4 \
  --warmup 2 \
  --runs 10

Use --format csv or --format jsonl for machine-readable output. Every row includes the Git revision, Crystal version, release flag, CPU count, workload dimensions, end-to-end throughput, client allocation and CPU deltas, ask latency percentiles, and separate server allocation/CPU deltas for two-process remoting. The two-process topology starts and gracefully stops a child server using the same benchmark executable.

The published documentation contains the end-user guides and API reference.

Repository

movie-cr

Owner
Statistic
  • 0
  • 0
  • 2
  • 1
  • 2
  • 2 days ago
  • May 18, 2026
License

MIT License

Links
Synced at

Sun, 06 Sep 2026 14:09:48 GMT

Languages