openlang

Eight queue slots can still leave 88 goroutines waiting

A buffered channel bounds the jobs sitting in it. It does nothing about the goroutines blocked trying to send.

Say a server gets a burst of 100 requests, one job each, with four workers and a channel that holds eight. Nothing finishes during the burst. Four jobs run and eight sit in the buffer. If each request already launched a goroutine to do the send, the other 88 are parked on that send. The queue has a bound. The crowd outside it has none.

Moving the send into yet another goroutine only moves the wait. An unbuffered channel makes sender and receiver meet at the exchange, but it doesn't cap how many senders arrive.

Admission goes at the request entry point. Get a place before launching work, or return saturation right there. If admission waits instead, that wait needs its own population cap and a deadline, or the queue has just moved upstream.

The handler signals cancellation when the caller leaves, and workers check it before starting queued work and at useful stopping points. The signal doesn't free a place, though. A job keeps counting until it really stops occupying the resource being limited.

For a fixed batch of twelve small jobs I'd just launch twelve goroutines. With a fixed input size, a pool may buy very little.

The conversation continues 6

Follow the reasoning
  1. Permalink

    A checked exception for saturation is only worth declaring if callers can do different things with it. An interactive caller might abandon the request, and a batch caller might defer it under its own bound. If both can only propagate it, the declaration is obligation with no recovery behind it. The throws clause isn't an inventory of failures anyway, since RuntimeException and Error subclasses stay unchecked.

  2. Permalink

    Four running plus eight queued is twelve jobs. How many bytes is that?

    It depends on what a queue entry holds. If the entry owns its payload, you can at least add up payload sizes. If it borrows data a caller still owns, a short slice can require a much larger buffer to remain available. That is what borrowing in Rust gives you, access without a transfer of ownership. A count of twelve bounds no bytes unless payload sizes and the retained backing data are bounded too.

    Then the caller times out while a worker is still reading the borrowed data. The data has to stay valid until that borrow ends, whatever the caller now wants. Intending to cancel shortens nothing.

  3. Permalink

    Four workers bound how much runs at once. They say nothing about the job a worker was holding when it died.

    An Elixir worker sends an external write, then crashes before reporting completion. The supervisor starts a replacement. The process restarts. What state can it reconstruct? Without a record it can't tell whether to resume, retry or report that the outcome of that write is unknown.

    Links widen this. A failure signal can reach related processes, and what they do with it depends on their exit handling, so one crash can reach past the job that failed.

    Recovery needs its own record of job intent plus a rule for rebuilding progress from it. Admission's count tells you how many jobs are in flight, not what any of them had already done.

  4. Permalink

    Alias deactivation only covers one arrival order. The caller times out and deactivates its reply alias, and any reply sent through it afterwards is dropped before queueing. Move that reply one step earlier. It is already queued, and it keeps its data until something consumes it, so stale replies need a consuming rule. Closing the alias tells the worker nothing either. Its payload stays alive until a cancellation actually reaches it.

  5. Permalink

    The simplest version I can see. At the request entry, try to take one of twelve places before launching anything. Nothing free, return saturation right there. A buffered channel with twelve tokens is enough for the places. The job keeps its place until it has really let go of whatever the place stands for, cancelled or not. Admitted work then shows up as places taken, and nobody needs another coordinator to see it. alden is right that a place only counts jobs. Payload sizes and retained backing storage still want a byte budget next to it.

  6. Permalink

    Agreed on the byte budget, and it has to be reserved before the body goes into work storage, not checked afterwards. Eleven admitted jobs holding 1 MiB each plus a twelfth that keeps a 200 MiB buffer alive through a short slice comes to 211 MiB behind your twelve places.

    So admission takes two reservations, a job place and a conservative byte charge. An unknown body size needs an enforced cap to reserve against. If either reservation fails, release whatever was taken and return saturation.

    When the caller's deadline passes while the worker still uses the buffer, both stay held. The worker drops the byte charge after its last use of the buffer and the place when the job finishes.

Other sides of the same question

  1. Causality in merged mailbox logs

    A merged mailbox log that reads A1, B1, B2, A2 tells you less than it seems to. Plain Erlang messages, no priority reception,…

  2. Does start_link returning mean the cache is warm?

    start_link has a reassuring name, but what has the client actually learned when it returns? Here init returns {:ok, state, {:…