---
title: "Streaming"
description: "A tumbling window per entity, whose state, output and acknowledgements commit together."
---

> Queen MQ documentation, for AI agents
> Complete self-contained summary of Queen MQ: https://queenmq.com/llms-brief.txt
> Fetch that first when the question is about the product rather than about this page.
> Index of all pages: https://queenmq.com/llms.txt

# Streaming

The four tutorials before this one move messages. This one aggregates them, and the aggregation
lives in the same database as the queue: window state per partition, each closed window pushed to
an output queue, and the acknowledgement of the inputs it was computed from, all in one PostgreSQL
transaction. That is exactly-once aggregation with no changelog topic and no state store to size
and operate.

Where the JavaScript client takes a single options object, this one spells the query as a chain:
`window_tumbling` and `idle_flush_ms` for the window, one call per aggregate, `to` for the sink,
then `run`. `run` hands back a handle, and `stop` consumes it, so a stopped stream cannot be
restarted by mistake.

A stream is a running process, which decides the order of the program. Its cursor is created by
the runner's first poll and starts at the tail, so a backlog already in the queue is not picked
up: the stream is started, given a poll window, and only then are the sales pushed.

The aggregate extractors are handed a `Record` over the message payload, not the envelope, so it
is `r.number("amount")`. A field that is missing or is not a number reads as `None` and is left
out of the total instead of failing, which is what makes getting this wrong quiet.

```rust title="examples/tutorials/rust/src/bin/05_streaming.rs"
//
// Tutorial 5 of 5: streaming.
//
// The four tutorials before this one move messages. This one aggregates them:
// a tumbling window per entity, whose state, output and acknowledgements commit
// in the same PostgreSQL transaction. That is exactly-once aggregation with no
// changelog topic and no state store to operate, because the state and the
// queue are already in the same database.
//
// A stream is a running process, so the order here is the order you would use
// in production: start it, then let events arrive.
//
// Run it:
//   QUEEN_URL=http://localhost:6632 cargo run --bin 05_streaming

use std::collections::HashMap;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use queen_mq::streams::{RunOptions, Stream};
use queen_mq::{Config, Queen, QueueOptions, SubscriptionMode};
use serde_json::json;

// (customer, amount)
const SALES: [(&str, f64); 5] = [
    ("acme", 10.0),
    ("acme", 32.5),
    ("globex", 7.25),
    ("acme", 0.25),
    ("globex", 100.0),
];

struct Checks(usize);

impl Checks {
    fn assert(&mut self, condition: bool, description: &str) -> Result<(), String> {
        if !condition {
            return Err(description.to_string());
        }
        self.0 += 1;
        println!("  ok: {description}");
        Ok(())
    }
}

#[tokio::main]
async fn main() {
    match run().await {
        Ok(checks) => println!("\nPASS: {checks} checks"),
        Err(e) => {
            eprintln!("\nFAIL: {e}");
            std::process::exit(1);
        }
    }
}

async fn run() -> Result<usize, String> {
    let url = std::env::var("QUEEN_URL").unwrap_or_else(|_| "http://localhost:6632".into());
    let run_id = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_millis();
    let events = format!("tut-rust-stream-events-{run_id}");
    let totals = format!("tut-rust-stream-totals-{run_id}");

    // The query id is this streaming query's identity in the database. Its
    // window state is keyed by it, so restarting the program with the same id
    // resumes the same windows instead of starting new ones. The runner also
    // derives its consumer group from it, as `streams.{query_id}`.
    let query_id = format!("tut-rust-totals-{run_id}");

    let mut checks = Checks(0);
    println!("broker {url}");

    let queen = Queen::connect(Config::new(&url)).map_err(|e| e.to_string())?;

    // Both queues are created up front here, rather than by the first push, so
    // the stream has something to attach to before any event exists.
    // configure() is a full replace: keys left out go back to the broker's
    // defaults rather than keeping a previous value.
    queen
        .queue(&events)
        .configure(QueueOptions {
            lease_time: Some(30),
            retry_limit: Some(3),
            ..Default::default()
        })
        .await
        .map_err(|e| e.to_string())?;
    queen
        .queue(&totals)
        .configure(QueueOptions {
            lease_time: Some(30),
            ..Default::default()
        })
        .await
        .map_err(|e| e.to_string())?;

    println!("\nstarting the stream");
    let handle = Stream::from(queen.queue(&events))
        // Tumbling: fixed, non-overlapping windows, one set per partition. A
        // window closes when its time is up; idle_flush_ms also closes one whose
        // partition has gone quiet, which is what lets a short program finish.
        // Where the JavaScript client takes one options object, this client
        // spells the window and its knobs as separate steps in the chain.
        .window_tumbling(2)
        .idle_flush_ms(800)
        // The extractors receive a Record over the message payload, not the
        // envelope: it is r.number("amount"), not the message's `data` field.
        // A missing or non-numeric field yields None and is simply not
        // accumulated.
        .aggregate_count("count")
        .aggregate_sum("sum", |r| r.number("amount"))
        .aggregate_max("max", |r| r.number("amount"))
        // Every closed window is pushed here, in the same transaction that
        // commits the window state and acknowledges the inputs it was computed
        // from. The sink keeps the source partition, so a window lands in the
        // lane it was computed for.
        .to(queen.queue(&totals))
        // The chain's shape is hashed and stored with the query. Redeploying a
        // changed chain against the same id is refused rather than silently
        // reusing state computed under other semantics; reset(true) is the way
        // to say you meant it.
        .run(
            &queen,
            RunOptions::new(&query_id)
                .batch_size(100)
                .max_partitions(8)
                .max_wait(Duration::from_millis(200)),
        )
        .await
        .map_err(|e| e.to_string())?;

    // run() registers the query and spawns the poll loop, but does not wait for
    // its first poll — and it is that first poll which creates the runner's
    // cursor. A new cursor starts at the tail, so events pushed into the gap
    // would be skipped. Waiting one poll window closes it.
    tokio::time::sleep(Duration::from_millis(500)).await;

    println!("\npushing sales");
    for (customer, amount) in SALES {
        // The partition is the aggregation key: window state is per partition,
        // so one customer's totals are computed from that customer's lane
        // alone.
        queen
            .queue(&events)
            .partition(customer)
            .push(json!({ "customer": customer, "amount": amount }))
            .await
            .map_err(|e| e.to_string())?;
        println!("  {customer} {amount}");
    }

    println!("\ncollecting closed windows");

    // A window is a slice of time, so a customer's sales can fall on either
    // side of a boundary and arrive as two windows instead of one. That is what
    // windowing is, and it is why this adds the windows up per customer instead
    // of expecting exactly one each.
    //
    // The loop waits for the totals it expects, with a deadline. Waiting for a
    // quiet period instead would be a race: the last window closes when its
    // timer says so, not when the reader is tired of waiting. That is also why
    // this is a pop loop rather than consume(): a consumer stopped by limit() or
    // idle() decides for itself when it has seen enough, and here the program is
    // the one that knows.
    #[derive(Default, Clone, Copy)]
    struct Totals {
        count: f64,
        sum: f64,
        max: f64,
    }

    let mut totals_seen: HashMap<String, Totals> = HashMap::new();
    let complete = |seen: &HashMap<String, Totals>| {
        seen.get("acme").map(|t| t.count) == Some(3.0)
            && seen.get("globex").map(|t| t.count) == Some(2.0)
    };
    let deadline = Instant::now() + Duration::from_secs(30);

    while !complete(&totals_seen) && Instant::now() < deadline {
        let closed = queen
            .queue(&totals)
            .group("tut-rust-stream-collector")
            .subscription_mode(SubscriptionMode::All)
            .batch(20)
            .partitions(10)
            .wait(true)
            .poll_timeout(Duration::from_secs(2))
            .pop()
            .await
            .map_err(|e| e.to_string())?;

        for msg in &closed {
            // The window's key is the partition it was computed for.
            println!("  {}: {}", msg.partition, msg.data);
            let t = totals_seen.entry(msg.partition.clone()).or_default();
            t.count += msg.data["count"].as_f64().unwrap_or(0.0);
            t.sum += msg.data["sum"].as_f64().unwrap_or(0.0);
            t.max = t.max.max(msg.data["max"].as_f64().unwrap_or(0.0));

            // pop() takes a lease and leaves it to you; only consume() settles
            // on your behalf. Acking here commits the collector's cursor, so a
            // window is counted once even though the loop polls repeatedly.
            queen.ack(msg).await.map_err(|e| e.to_string())?;
        }
    }

    // Stop the runner before checking, so nothing is still writing to the
    // queues the assertions read. stop() waits for the in-flight cycle and its
    // flush, and it consumes the handle: a stopped stream cannot be restarted
    // by mistake.
    handle.stop().await.map_err(|e| e.to_string())?;

    checks.assert(
        complete(&totals_seen),
        "every sale reached a closed window before the deadline",
    )?;
    let acme = totals_seen["acme"];
    let globex = totals_seen["globex"];

    checks.assert(
        (acme.sum - 42.75).abs() < 0.001,
        "acme summed to 42.75 across its windows",
    )?;
    checks.assert(
        (globex.sum - 107.25).abs() < 0.001,
        "globex summed to 107.25",
    )?;
    checks.assert(
        globex.max == 100.0,
        "globex kept its largest single sale",
    )?;

    queen
        .queue(&events)
        .delete()
        .await
        .map_err(|e| e.to_string())?;
    queen
        .queue(&totals)
        .delete()
        .await
        .map_err(|e| e.to_string())?;

    queen.close().await.map_err(|e| e.to_string())?;

    Ok(checks.0)
}
```

## Run it

Against a broker from [the quickstart](/start/quickstart), from `examples/tutorials/rust`:

```bash
QUEEN_URL=http://localhost:6632 cargo run --bin 05_streaming
```

The program checks its own outcome and exits non-zero if a check fails. Every tutorial in this
section runs in the repository's own suite: `examples/tutorials/run.sh rust`.

Source: https://queenmq.com/use/rust-client/streaming/index.mdx
