Skip to content

bug(podman): watch initialization hangs with more than 256 existing sandbox snapshots #4717

Open

Description

@AdityaProMax

Summary

The Podman driver's start_watch() can deadlock during initial state synchronization when more than 256 existing workload containers produce valid sandbox snapshots.

The initial snapshot loop awaits sends into a bounded Tokio channel with capacity 256 before returning the receiver to its caller. Once 256 messages have been enqueued, send 257 waits for a receiver that the caller cannot yet access.

This is supported by source inspection and a minimal reproduction using real Tokio. An end-to-end OpenShell/Podman reproduction has not yet been run.

Source and failure sequence

Source: watcher.rs on main, inspected October 19, 2026. The same pattern is present in v0.1.4.

  1. start_watch() creates mpsc::channel::<Result<WatchSandboxesEvent, ComputeDriverError>>(256).
  2. It subscribes to Podman events and lists existing managed workload containers.
  3. The initial inventory loop calls tx.send(...).await once per eligible sandbox snapshot.
  4. The 257th send waits for channel capacity.
  5. ReceiverStream::new(rx) is returned only after that loop completes, so the consumer cannot drain the channel.

The threshold concerns emitted initial snapshots, not the number of raw Podman containers or a 256-sandbox limit on an already-established watch.

Minimal Tokio reproduction

Executed locally with Rust 1.99.1, Tokio 1.53.3, and four Tokio worker threads. This isolates the channel ordering; it does not run OpenShell or Podman.

Cargo.toml:

[package]
name = "watch-channel-repro"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "=1.53.3", features = ["macros", "rt-multi-thread", "sync", "time"] }

The essential reproduction is:

use tokio::{sync::mpsc, time::{timeout, Duration}};

async fn start_watch(n: usize) -> mpsc::Receiver<usize> {
    let (tx, rx) = mpsc::channel(256);
    for sandbox in 0..n {
        tx.send(sandbox).await.unwrap();
    }
    rx
}

#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
    for n in [255, 256, 257, 1000] {
        let result = timeout(Duration::from_millis(300), start_watch(n)).await;
        println!("n={n}: returned_receiver={}", result.is_ok());
        assert_eq!(result.is_ok(), n <= 256);
    }
}

In the executed harness, an atomic counter after each successful send additionally confirmed that both failing cases completed exactly 256 sends and timed out waiting on send 257.

Initial snapshots Existing ordering Concurrent snapshot producer
255 Completes —
256 Completes —
257 Times out on send 257 Completes
1,000 Times out on send 257 Completes
10,000 — Completes

The concurrent-producer variant retained channel capacity 256, spawned the sending loop, returned the receiver immediately, and drained all messages in the caller.

Expected behavior and impact

Watch initialization should complete with an inventory larger than the output channel capacity.

A pool may grow beyond 256 eligible sandboxes while an established watch is consuming events normally, then encounter this problem on gateway/watch startup or reconnection when the existing inventory must be replayed. Existing workload processes need not stop, but the affected watch cannot finish initialization.

Suggested fix and regression coverage

Allow the initial snapshot producer and receiver consumer to run concurrently, while preserving subscription-before-list ordering and initial-snapshot-before-live-event delivery. For example, move snapshot production and subsequent live-event forwarding into a producer task, then return the receiver promptly. Preserve cancellation, receiver-drop, and error handling.

Increasing channel capacity only moves the threshold.

A regression test should initialize a watch against more than 256 valid existing sandbox records and verify, under a timeout, that the watch returns and the complete initial inventory can be consumed.

Activity

  1. letv1nnn commented on Oct 20, 2026

    @letv1nnn
    Contributor

    📋 triage-agent

    Assessment: technically confirmed defect; first-hand context requested.

    The failure sequence matches main (eeba0e79): crates/openshell-driver-podman/src/watcher.rs:136 creates a 256-slot channel, the initial-sync loop awaits each send, and the receiver is returned only at :222.

    Reproduced through OpenShell code (not only standalone Tokio): calling the real start_watch() against the driver's Podman HTTP stub (test_utils::spawn_podman_stub) with N exited, managed workload containers, under a 3 s timeout:

    N Result
    255 / 256 returns, all snapshots delivered
    257 / 300 hangs

    Impact is likely wider than stated: neither the gateway's driver watch (openshell-server/src/compute/mod.rs:179) nor the Podman gRPC wrapper (grpc.rs:222) times out opening the watch, so the call never returns or errors. No reconnect is triggered, and the gateway receives no Podman sandbox updates at all.

    Not yet verified: an end-to-end run against a real Podman with >256 sandboxes.

    Requested: the issue has no User Story. @AdityaProMax, do you run OpenShell with the Podman driver at this scale, or expect to? Please describe the deployment and how you hit (or would hit) this.

    Next human decision: a maintainer decides whether to accept. The fix proposed in the issue (move initial sync into the producer task, keep subscribe-before-list and initial-before-live ordering) is small and confined to start_watch(); a stub-based regression test is feasible.

  2. letv1nnn commented on Oct 20, 2026

    @letv1nnn
    Contributor

    hey @AdityaProMax, I'd like to pick this up if no one is already on it.

  3. AdityaCohere commented on Oct 20, 2026

    @AdityaCohere

    Hey @letv1nnn I have a fix for this ready and tested, was wondering if you'd be able to accept the issue so I can make a PR from my branch?

  4. letv1nnn commented on Oct 21, 2026

    @letv1nnn
    Contributor

    hey @AdityaProMax, I'm just a contributor, not a maintainer, so I can't accept issues, sorry, but you can still open a PR, since this is a valid issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions