initwrite

package
v9.0.0-rc.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package initwrite provides a progress-aware write deadline for initialization-delivery responses -- SSE stream replays and poll responses. It bounds how long a slow or stalled client can hold a budget slot without false-killing a slow-but-steady client: a minimum-throughput floor drives a per-write deadline, and an absolute cap bounds a single delivery.

A Writer must be constructed with Wrap or WrapGated; the zero value is not usable.

Connection ownership. Only the goroutine running the HTTP handler may set or clear the write deadline, and it does so through this Writer: arming it before each write and clearing it at the end-of-delivery flush. A write deadline is a capability scoped to the handler's lifetime -- after the handler returns, the underlying connection may be recycled or, on HTTP/2, gone entirely, so touching it then is unsafe. For a gated stream the producer goroutine that feeds events can outlive the handler (the SSE server drains it once the client leaves), so it must never touch the connection; it coordinates only through Begin/End/Done/WaitAndFinish. That keeps every deadline call within the handler's lifetime.

Two shapes are supported:

  • Poll (Wrap): a request/response delivery. The deadline is armed on every write for the lifetime of the wrapper. net/http resets the connection's write deadline when the handler returns, so it does not linger onto a later request on a kept-alive connection.
  • Stream (WrapGated): a persistent SSE connection, where the connection's write deadline is not reset between the initial delivery and later delta or heartbeat traffic. The deadline is armed only between Begin and the end-of-delivery flush, and is cleared there, so live traffic and idle periods carry no deadline. This matters on HTTP/2, where a lingering deadline is a self-firing timer that would reset an otherwise idle stream. Begin, End, Done and WaitAndFinish apply only to this shape.

Ending a gated delivery. The producer must call End on EVERY exit after Begin -- whether it wrote the whole basis, or is abandoning the delivery after an error -- and before it closes the batch channel. End marks the delivery finished; the handler's end-of-delivery flush then clears the deadline and releases the waiter. There are three ways a delivery ends, and End is what makes the first two safe:

  • Clean finish: the producer wrote everything, calls End, and the flush clears the deadline, leaving the now-idle stream alone.
  • Producer error with the client still healthy: the producer calls End on the error path too -- even an exit that wrote nothing, since the delivery stays active and a later heartbeat write would arm a deadline that then fires with nothing left to finish it. Skipping End is a bug: the deadline is never cleared, and the healthy stream is killed anyway -- on HTTP/1.1 at the first write after the absolute cap expires, and on HTTP/2 within a few seconds (the write slack) of the last write, because a deadline there is a self-firing timer. (A heartbeat interval shorter than the slack keeps postponing the HTTP/2 fire, so the bug can be invisible under fast test heartbeats yet fatal at a production interval.) Disabling the cap does not make the omission safe: HTTP/2 still dies on the slack timer, while on HTTP/1.1 nothing fires at all and the budget slot is pinned permanently instead.
  • Client goes away first: WaitAndFinish returns on context cancellation without touching the connection; the per-write deadline already armed bounds anything still in flight, and the connection teardown clears it.

The producer waits via WaitAndFinish, which frees the budget slot once the delivery has been flushed or the client has gone.

A client sustaining at least the throughput floor per write keeps re-arming and is not cut for being slow; one that stalls or drops below the floor on a write has its write fail, which the HTTP server turns into a closed connection. The absolute cap (maxHold, supplied by the caller) backstops a client that stays right at the floor on a very large payload: once a delivery would exceed maxHold at the floor, the cap governs and the client is cut. A maxHold of zero or less disables the cap, leaving only the per-write floor; callers that want the backstop must pass a positive value.

Index

Constants

View Source
const WriteSlack = 5 * time.Second

WriteSlack is the fixed allowance added to each per-write deadline. It is exported so a caller that configures an absolute delivery cap can keep that cap at or above it; a cap below the slack would expire before even a small first write.

Variables

This section is empty.

Functions

This section is empty.

Types

type Writer

type Writer struct {
	http.ResponseWriter
	// contains filtered or unexported fields
}

Writer wraps an http.ResponseWriter and arms a progress-aware write deadline on the underlying connection while a delivery is active. Construct it with Wrap or WrapGated.

func Wrap

func Wrap(w http.ResponseWriter, maxHold time.Duration) *Writer

Wrap returns a Writer for a poll (request/response) delivery: the deadline is armed on every write. net/http resets the connection deadline when the handler returns.

func WrapGated

func WrapGated(w http.ResponseWriter, maxHold time.Duration) *Writer

WrapGated returns a Writer for a persistent stream: it arms nothing until Begin, and clears the deadline at the End-triggered end-of-delivery flush.

func (*Writer) Begin

func (w *Writer) Begin()

Begin marks the start of a gated delivery; writes from here on arm the deadline. It is called from the producer before it starts producing events. If a previous delivery never completed, its Done channel is closed here so any waiter is released rather than orphaned.

func (*Writer) CapEngaged

func (w *Writer) CapEngaged() bool

CapEngaged reports whether the absolute cap clamped a deadline for the current delivery. When it is true, the delivery is large enough that the cap, not the throughput floor, decides when a slow client is cut.

func (*Writer) Cut

func (w *Writer) Cut()

Cut marks the connection as cut: the delivery in progress -- or the next one to begin -- has its writes fail at once instead of draining until a per-chunk deadline, because the client is gone. The mark is terminal; there is no way back. With a delivery in progress the write deadline moves to now immediately; otherwise the deadline is left alone (so a connection that never carried a gated delivery still closes gracefully) and the first write of a later delivery applies it instead.

A write deadline is a capability scoped to the handler's lifetime (see the package comment), so Cut must be called only from the handler goroutine, or from a goroutine that provably cannot outlive the handler. The producer goroutine must never call it.

func (*Writer) DeadlineSetErrors

func (w *Writer) DeadlineSetErrors() int64

DeadlineSetErrors returns how many SetWriteDeadline calls have failed on this connection. A value above zero means the deadline protection is not in force.

func (*Writer) Done

func (w *Writer) Done() <-chan struct{}

Done returns a channel closed once the current gated delivery has ended. The channel is never nil: with no delivery in progress it is already closed, so a producer that waits on it is not left blocked. Call it only after Begin -- capturing it earlier returns the closed idle channel and would release the slot while the delivery is still in flight. Prefer WaitAndFinish, which reads it at the right time.

func (*Writer) End

func (w *Writer) End()

End marks the delivery finished so the next flush tears it down. Call it on every exit after Begin -- a completed delivery AND an abandoned one (a serialization or store error), even one that wrote nothing -- and before closing the batch channel, so the handler's single end-of-batch flush observes it and clears the deadline. It is idempotent. When using defers, register the batch-channel close before this one (defer close(out), then defer w.End()), so End runs first. Omitting it on an error path leaves the armed deadline in place, which eventually cuts even a healthy client; releasing the slot without it is not enough, because only the handler goroutine can clear the connection's deadline.

func (*Writer) Flush

func (w *Writer) Flush()

Flush flushes buffered bytes and, at the end-of-delivery flush (after End), clears the deadline so later traffic on a persistent stream is not governed by it. The clear runs under the lock and only for the delivery that was active when Flush was entered -- the state is sampled BEFORE flushing, so a delivery that begins during the flush is untouched, and a stale flush cannot wipe a newer delivery's deadline. Flush runs on the handler goroutine, within the connection's lifetime. The teardown is deferred, so it frees the slot and clears the deadline even if the flush itself is unsupported, fails, or panics.

func (*Writer) Unwrap

func (w *Writer) Unwrap() http.ResponseWriter

Unwrap exposes the wrapped ResponseWriter so http.NewResponseController and other wrappers can reach the underlying connection through this one.

func (*Writer) WaitAndFinish

func (w *Writer) WaitAndFinish(ctx context.Context) bool

WaitAndFinish holds until the current gated delivery's last byte has been flushed to the client (Done closes, the handler having cleared the deadline) or ctx is done (the client went away). The caller releases its budget slot once this returns. It never touches the connection -- on cancellation the already-armed per-write deadline bounds anything still in flight and the connection teardown clears it -- so it is safe on the producer goroutine even after the handler has returned. Call it after Begin. It must be given the request's context, or another that is cancelled when the client disconnects: a never-cancelled context would block the producer, and its slot, until the delivery flushes, and a context cancelled while the handler is still live (a shutdown or producer-error signal, say) would return early without ending the delivery, leaving the missed-End failure described in the package comment -- use End for that.

The return value reports the outcome: true when the delivery's last byte was flushed to the client, false when the connection ended first. A false return does not say why the connection ended -- a client disconnect and a relay deadline cut look the same here.

func (*Writer) Write

func (w *Writer) Write(p []byte) (int, error)

Write slices p into chunks and arms the per-chunk deadline before each.

func (*Writer) WriteString

func (w *Writer) WriteString(s string) (int, error)

WriteString satisfies io.StringWriter so that when the wrapped writer also implements it, eventsource's io.WriteString does not allocate a []byte copy of the whole payload; it slices the string and, at worst, copies at most one chunk at a time.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL