You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Explore if we can have a single binary + a cluster/microservice mode, similar to VictoriaMetrics #102
Right now, Controller, Arroyo, and QueryEngineRust are built and deployed separately. This is great for scale out, but difficult to develop and deploy by a beginner user. Similar to VictoriaMetrics, we can think of both a single-node single-binary application, and a cluster version that has different components that can be scaled independently.
Unified Binary Architecture: Single-Node and Cluster Deployment
Goal
Create a unified binary (asap) that can run in two deployment modes:
Single-Node Mode: All components (query engine + streaming engine) run in a single process with in-memory communication
Cluster Mode: Components run as independent, horizontally-scalable microservices communicating over the network (Kafka/gRPC)
Additionally, the architecture should allow swapping the streaming engine (currently Arroyo) for a custom mini-batch engine in the future without modifying core query engine logic.
Analogy: VictoriaMetrics Architecture
VictoriaMetrics implements this exact pattern and serves as our reference architecture.
VictoriaMetrics Structure
Mode
Components
Communication
Single-Node
One victoria-metrics binary
Direct function calls, shared memory
Cluster
Separate vminsert, vmselect, vmstorage binaries
Network RPC (gRPC/HTTP)
Key Design Principles from VictoriaMetrics
1. Core Logic is Deployment-Agnostic
The storage engine (lib/storage) has no knowledge of whether it's running in single-node or cluster mode. It exposes the same API regardless:
// Same interface used in both modesstorage.SetMinScrapeIntervalForDeduplication(duration)
storage.SetBigMergeWorkersCount(count)
2. Network Layer is a Thin Wrapper
In cluster mode, the network layer simply wraps the same core operations with RPC serialization. The core logic remains unchanged.
3. Unified Configuration
Both modes use identical configuration patterns (flags + environment variables). The storage layer receives the same configuration values regardless of how they were provided or which mode is running.
4. ~90-95% Code Reuse
The vast majority of code lives in shared libraries (lib/). Only the thin orchestration and networking layers differ between modes.
Current State: QueryEngineRust + Arroyo
┌─────────────────────┐ ┌─────────────────────┐
│ QueryEngineRust │ │ Arroyo │
│ (query serving) │◄─Kafka──│ (stream processing)│
└─────────────────────┘ └─────────────────────┘
│ │
└────── Separate binaries ───────┘
Separate deployments
Network overhead even
when co-located
Current Pain Points:
Two separate binaries to build, deploy, and monitor
Kafka overhead even when running on the same machine
No easy way to run a lightweight local/dev setup
Tight coupling to Arroyo makes future engine changes difficult
Run everything locally with asap --mode single-node
Flexible Scaling
Same binary, different flags for cluster mode
Unified Monitoring
Single process = single set of metrics in simple deployments
Architectural Benefits
Benefit
Description
Engine Swappability
Replace Arroyo with custom mini-batch engine without touching query layer
Clear Boundaries
Traits enforce separation between core logic and I/O adapters
Testability
Mock implementations for unit/integration testing
Code Reuse
Core logic shared across all deployment modes
Developer Experience
Benefit
Description
Quick Start
asap just works out of the box for evaluation
Gradual Complexity
Start single-node, migrate to cluster when needed
Consistent Behavior
Same core logic guarantees identical results across modes
Core Abstractions
StreamingEngine Trait
The key abstraction that enables engine swappability:
#[async_trait]pubtraitStreamingEngine:Send + Sync{/// Start processing, sending results to the provided sinkasyncfnrun(&self,sink:Box<dynPrecomputeSink>) -> Result<()>;/// Graceful shutdownasyncfnshutdown(&self) -> Result<()>;/// Health status for orchestrationfnhealth(&self) -> EngineHealth;}
PrecomputeSink Trait
Abstracts the destination for precomputed results:
#[async_trait]pubtraitPrecomputeSink:Send + Sync{asyncfnsend(&self,output:PrecomputedOutput) -> Result<()>;}// Implementations:// - ChannelSink: in-memory mpsc channel (single-node)// - KafkaSink: Kafka producer (cluster mode)// - DirectSink: direct function call to store (maximum performance)
IngestPort Trait
Abstracts how the query engine receives precomputed data:
This document summarizes the findings from investigating whether Arroyo can be embedded as a library within QueryEngineRust to create a unified single-node binary.
Executive Summary
Embedding Arroyo as a library faces three critical blockers related to its distributed architecture assumptions. Arroyo was designed as a distributed system where workers coordinate with a central controller - it has no standalone/embedded mode.
Critical Risks
1. Worker Requires Controller (CRITICAL)
Finding: The Arroyo worker cannot start without an active Controller.
Evidence (arroyo-worker/src/lib.rs:225-231):
letmut client = retry!(
controller_client("worker",&config.worker.tls).await,2,Duration::from_millis(100),Duration::from_secs(2),
|e| warn!("Failed to connect to controller: {e}, retrying..."))?;
Impact:
Worker blocks on startup until controller responds
Continuous gRPC heartbeat every 5 seconds (run_control_loop)
Job assignments come from controller, not local configuration
Cannot run a worker in isolation
Required to fix: Remove/mock controller client, inject job config directly, implement local control loop.
2. Global Singletons (CRITICAL)
Finding: Multiple lazy_static, OnceLock, and ArcSwap singletons prevent multiple instances and cause initialization conflicts.
lazy_static!{pubstatic ref MESSAGE_RECV_COUNTER:IntCounterVec = register_int_counter_vec!(...).unwrap();pubstatic ref MESSAGES_SENT:IntCounterVec = register_int_counter_vec!(...).unwrap();// 6+ more global counters}
Other globals:
STORAGE_PROVIDER in arroyo-state (OnceCell)
DB_BACKUP_NOTIFIER in arroyo-rpc (OnceLock)
Various lazy_statics in arroyo-node, arroyo-controller
Impact:
Cannot run multiple Arroyo instances in one process
Config initialization panics if called twice
Prometheus metrics fail on duplicate registration
Parent binary cannot use same metric names
3. Configuration Namespace Pollution (CRITICAL)
Finding: Arroyo uses ARROYO__* environment variable prefix that can conflict with parent application.
Evidence (arroyo-rpc/src/config.rs:141-190):
// Priority (highest to lowest):// 1. ARROYO__* environment variables <-- Will override everything// 2. Config file specified in code// 3. *.toml or *.yaml in config directory// ...
Impact:
Any ARROYO__* env var in the environment overrides programmatic config
Cannot isolate Arroyo config from parent application
Deployment environment may have conflicting variables
Example conflict:
# If parent application or environment has:export ARROYO__WORKER__ID=999
export ARROYO__CHECKPOINT_URL=s3://other-bucket
# Arroyo will use these instead of intended values
Medium Risks
4. Hardcoded Connector Registry
Finding: Sinks are registered in a hardcoded list, not dynamically.
Start with Option A - it achieves the single-binary deployment goal with minimal risk. The internal gRPC/Kafka overhead within localhost is negligible for most use cases.
Pursue Option B only if profiling shows the internal communication is a bottleneck.
Option C aligns with the long-term goal of a custom mini-batch engine but should be deferred until that work begins.
References
arroyo-worker/src/lib.rs - Worker initialization and control loop
arroyo-rpc/src/config.rs - Configuration loading and globals
Activity
milindsrivastava1997 commentedon Mar 4, 2026
PRO-367
milindsrivastava1997 commentedon Mar 4, 2026
Unified Binary Architecture: Single-Node and Cluster Deployment
Goal
Create a unified binary (
asap) that can run in two deployment modes:Additionally, the architecture should allow swapping the streaming engine (currently Arroyo) for a custom mini-batch engine in the future without modifying core query engine logic.
Analogy: VictoriaMetrics Architecture
VictoriaMetrics implements this exact pattern and serves as our reference architecture.
VictoriaMetrics Structure
victoria-metricsbinaryvminsert,vmselect,vmstoragebinariesKey Design Principles from VictoriaMetrics
1. Core Logic is Deployment-Agnostic
The storage engine (
lib/storage) has no knowledge of whether it's running in single-node or cluster mode. It exposes the same API regardless:2. Network Layer is a Thin Wrapper
In cluster mode, the network layer simply wraps the same core operations with RPC serialization. The core logic remains unchanged.
3. Unified Configuration
Both modes use identical configuration patterns (flags + environment variables). The storage layer receives the same configuration values regardless of how they were provided or which mode is running.
4. ~90-95% Code Reuse
The vast majority of code lives in shared libraries (
lib/). Only the thin orchestration and networking layers differ between modes.Current State: QueryEngineRust + Arroyo
Current Pain Points:
Target Architecture
Benefits
Operational Benefits
asap --mode single-nodeArchitectural Benefits
Developer Experience
asapjust works out of the box for evaluationCore Abstractions
StreamingEngine Trait
The key abstraction that enables engine swappability:
PrecomputeSink Trait
Abstracts the destination for precomputed results:
IngestPort Trait
Abstracts how the query engine receives precomputed data:
Configuration Strategy
Following VictoriaMetrics' pattern: unified configuration with CLI flags, environment variables, and config files.
Unified Config File
CLI Usage
Directory Structure
Migration Path
Phase 1: Create Core Abstractions
Goal: Define traits without changing existing code behavior.
Tasks:
lib/corecrate withStreamingEngine,PrecomputeSink,IngestPorttraitsPrecomputedOutputand shared types tolib/corelib/corefor typesOutcome: Clean interfaces defined, existing code still works unchanged.
Phase 2: Create Arroyo Adapter
Goal: Wrap Arroyo behind the
StreamingEnginetrait.Tasks:
engines/arroyo-adaptercrateStreamingEnginetrait wrapping Arroyo's pipeline executionPrecomputeSinkthat writes to Arroyo's existing Kafka sinkOutcome: Arroyo can be used through the
StreamingEngineabstraction.Phase 3: Implement Sink/Ingest Adapters
Goal: Create the communication adapters for both modes.
Tasks:
ChannelSink(mpsc sender)ChannelIngest(mpsc receiver)KafkaConsumerto implementIngestPorttraitPrecomputeSinktraitOutcome: Both in-memory and Kafka communication paths available.
Phase 4: Create Unified Binary (Single-Node)
Goal: Single binary that runs everything in one process.
Tasks:
asapcratelib/configwith unified configuration schemaOutcome:
asap --mode single-nodeworks.Phase 5: Add Cluster Mode
Goal: Same binary can run individual components for independent scaling.
Tasks:
--componentflag to CLIrun_query_engine_only()(reads from Kafka)run_streaming_worker_only()(writes to Kafka)Outcome:
asap --mode cluster --component query-engineworks.Phase 6: (Future) Custom Mini-Batch Engine
Goal: Replace Arroyo with a custom, simpler streaming engine.
Tasks:
MiniBatchEnginewithStreamingEnginetraitengine.type: minibatchconfiguration supportOutcome: Can switch engines via configuration without code changes to query layer.
Success Criteria
asapruns full stack with one commandengine.typewithout query layer changesOpen Questions
State management in single-node mode: How should checkpointing work when Arroyo runs embedded? Use local filesystem?
Graceful shutdown coordination: In single-node mode, what's the shutdown sequence? Drain queries → checkpoint streaming → exit?
Resource isolation: Should single-node mode have configurable resource limits per component (e.g., streaming gets 4 cores, query gets 2)?
Hybrid mode: Is there value in a mode where query engine is embedded but streaming workers are distributed?
References
milindsrivastava1997 commentedon Mar 4, 2026
Arroyo Embedding Risks Assessment
This document summarizes the findings from investigating whether Arroyo can be embedded as a library within QueryEngineRust to create a unified single-node binary.
Executive Summary
Embedding Arroyo as a library faces three critical blockers related to its distributed architecture assumptions. Arroyo was designed as a distributed system where workers coordinate with a central controller - it has no standalone/embedded mode.
Critical Risks
1. Worker Requires Controller (CRITICAL)
Finding: The Arroyo worker cannot start without an active Controller.
Evidence (
arroyo-worker/src/lib.rs:225-231):Impact:
run_control_loop)Required to fix: Remove/mock controller client, inject job config directly, implement local control loop.
2. Global Singletons (CRITICAL)
Finding: Multiple
lazy_static,OnceLock, andArcSwapsingletons prevent multiple instances and cause initialization conflicts.Configuration singleton (
arroyo-rpc/src/config.rs:29):Metrics singletons (
arroyo-metrics/src/lib.rs:43-88):Other globals:
STORAGE_PROVIDERinarroyo-state(OnceCell)DB_BACKUP_NOTIFIERinarroyo-rpc(OnceLock)arroyo-node,arroyo-controllerImpact:
3. Configuration Namespace Pollution (CRITICAL)
Finding: Arroyo uses
ARROYO__*environment variable prefix that can conflict with parent application.Evidence (
arroyo-rpc/src/config.rs:141-190):Impact:
ARROYO__*env var in the environment overrides programmatic configExample conflict:
Medium Risks
4. Hardcoded Connector Registry
Finding: Sinks are registered in a hardcoded list, not dynamically.
Evidence (
arroyo-connectors/src/lib.rs:41-70):Impact: Adding a custom in-memory channel sink requires modifying this file or forking the crate.
Workaround: Fork
arroyo-connectorsor modify to support dynamic registration.5. Global Checkpoint Storage
Finding: Checkpoint storage provider is a global singleton.
Evidence (
arroyo-state/src/lib.rs:167-186):Impact:
checkpoint_urlbefore any checkpoint operationWorkaround: Disable checkpointing for embedded mode, or accept the constraint.
Architecture Diagram
Implications for Unified Binary
Revised Approach Options
Option A: Orchestrate Full Stack (Low Risk)
Run controller + worker + query engine in same binary, accept internal gRPC/Kafka.
Effort: ~500-800 lines | Arroyo changes: None
Option B: Fork Worker for Embedded Mode (Medium Risk)
Create standalone worker that accepts jobs directly without controller.
Effort: ~800-1300 lines | Arroyo changes: ~300-500 lines modified
Option C: Use Arroyo Operators as Library (High Effort)
Extract operator implementations, build custom execution engine.
Effort: ~1500-2500 lines | Arroyo changes: Moderate
Option D: Keep Separate Processes (No Risk)
Bundle via Docker Compose / Kubernetes, no code changes.
Effort: ~100-200 lines (deployment config) | Arroyo changes: None
Recommendation
Start with Option A - it achieves the single-binary deployment goal with minimal risk. The internal gRPC/Kafka overhead within localhost is negligible for most use cases.
Pursue Option B only if profiling shows the internal communication is a bottleneck.
Option C aligns with the long-term goal of a custom mini-batch engine but should be deferred until that work begins.
References
arroyo-worker/src/lib.rs- Worker initialization and control looparroyo-rpc/src/config.rs- Configuration loading and globalsarroyo-metrics/src/lib.rs- Prometheus metric singletonsarroyo-state/src/lib.rs- Checkpoint storage globalsarroyo-connectors/src/lib.rs- Connector registrymilindsrivastava1997 commentedon Mar 4, 2026
Possible candidates for creating an embeddable stream processing library as a replacement for arroyo+ArroyoSketch:
milindsrivastava1997 commentedon Mar 4, 2026
Some datafusion related links:
milindsrivastava1997 commentedon Mar 4, 2026
Pinging Micah would be a good option