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
Commit 0075138
Browse filesBrowse the repository at this point in the historyBrowse files
fix(harness): bound requests by one read deadline and the head, not by call slots
Delta security review of #20 at b2cceaa:
- D2: a call takes its concurrency slot only once its whole request has
arrived, and reading one request (head and body) has one wall-clock
deadline instead of a timeout per read, so trickling peers no longer hold
every slot. Requests being read are bounded by memory (256 MiB of heads
and bodies), refused 503 before any of them is read; at the connection
bound the connection idle the longest is closed instead of the newcomer.
- D5: the request line and headers are bounded at 32 KiB together (431).
- D1: a header line the parser could not read is refused, rather than
ending the parsed headers early.
- D3: a handler keeps its slot until it returns, past a 504, and the drain
at a signal waits for it.
- D4: the harness's own concurrency default is named as such, and the docs
say how it relates to the runtime's installation concurrency.
- D6: idle connections are kept 120 s, longer than Go's 90 s pool, and
advertised with Keep-Alive.
- D7/D8: the smuggling test covers 404, GET with a body and 503; tests wait
on conditions the harness makes observable instead of sleeping.
- Nits: Expect: 100-continue gets the refusal instead of 100 Continue;
HandlerFailure.code is read-only; the core-bounds test compares numbers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/protocol.md
+58-17Lines changed: 58 additions & 17 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -48,19 +48,58 @@ owner's body would be a field the owner never described.
48
48
49
49
The body is read only by a `Content-Length` written in ASCII digits, at most
50
50
`max-input-bytes`. A missing length is refused `411`; a `Transfer-Encoding`, a
51
-
repeated length, a sign or any other spelling is refused `400`, unread; a body
52
-
that ends short of its length is refused `400`. **Every answer but a `200`
51
+
repeated length, a sign or any other spelling is refused `400`, unread; so is a
52
+
request carrying a header line the parser could not read, which would otherwise
53
+
end the parsed headers early and hide every header after it. A body that ends
54
+
short of its length is refused `400`. A request asking `Expect: 100-continue` is
55
+
told the refusal instead of being invited to upload. **Every answer but a `200`
53
56
closes the connection** (`Connection: close`), so bytes a refused call did not
54
57
consume are never read as the next call — under whatever Work Context those
55
58
bytes name.
56
59
57
-
The harness bounds what a peer can hold. A read or write that stalls for
58
-
`IO_TIMEOUT_SECONDS` (10 s) ends the connection, answering `408` when it was a
59
-
body that stalled; the same timeout releases an idle kept-alive connection.
60
-
Simultaneous calls are bounded by the declaration's `concurrency`
61
-
(`DEFAULT_CONCURRENCY`, 32, when it declares 0, the facility default); a call
62
-
over the bound is refused `503` before the handler runs. Open connections are
63
-
bounded at twice that, and one over it is closed unread.
60
+
### What a peer can hold
61
+
62
+
Every bound below is in `codefly_runnable/protocol.py`, and each is the harness's
63
+
own: none is a value a placement resolved.
64
+
65
+
| Bound | Value | At the bound |
66
+
| --- | --- | --- |
67
+
| reading one request, head and body together (`REQUEST_READ_SECONDS`) | 10 s wall clock, from its first byte |`408` once the body has begun; a head that has not arrived is dropped unanswered |
68
+
| the request line and headers (`MAX_HEAD_BYTES`) | 32 KiB |`431`|
69
+
| requests being read at once (`READ_MEMORY_BYTES` ÷ (32 KiB + `max-input-bytes`), at most 1,024) | 256 MiB of heads and bodies: 248 readers at the default 1 MiB input |`503`, before any of the request is read |
70
+
| calls whose handler is running (the declared `concurrency`) |`HARNESS_CONCURRENCY`, 32, when the declaration states 0 |`503`|
71
+
| open connections (readers + concurrency + `IDLE_CONNECTIONS`, 64) | — | the connection idle the longest is closed to make room; a new one is never refused |
72
+
| an idle kept-alive connection (`IDLE_SECONDS`) | 120 s, advertised as `Keep-Alive: timeout=120`| closed |
73
+
| one write of an answer (`WRITE_STALL_SECONDS`) | 10 s | the connection is dropped |
74
+
75
+
The read deadline is one instant per request, not a bound on each read: a peer
76
+
sending a byte every few seconds is cut when the request's time is up, however
77
+
regularly it sends. A call takes its concurrency slot only once its whole
78
+
request has arrived, so a request still arriving holds no slot, and a slow peer
79
+
costs a reader — bounded by memory, released at the deadline — rather than a
80
+
call. A slot is held until the handler really returns, not until the call is
81
+
answered: a call answered `504` at its deadline keeps its slot while its handler
82
+
runs on, so the declared concurrency bounds author code running.
83
+
84
+
**Concurrency is not yet one number.** Core's declaration says `0` means the
85
+
facility default and defines none, so the harness admits 32 when the declaration
86
+
is silent. The durable-work runtime decides how many calls it dispatches from
87
+
its own installation material (1 to 200), not from the declaration. When the
88
+
runtime dispatches more than the harness admits, each excess call answers `503`:
89
+
an unproven outcome the caller recovers by its recovery policy. Until the
90
+
placement passes the number it resolved, declare `concurrency` and install with
91
+
the same number. A declaration of `0` is not refused: core's own scaffold
92
+
declares no concurrency, so refusing it would refuse every scaffolded runnable.
93
+
94
+
The idle timeout is longer than the idle timeout of the pool the durable-work
95
+
runtime calls through (Go's `net/http`, 90 s by default), so the caller retires
96
+
an idle connection before the harness closes it. The other order makes a call
97
+
written onto a connection the harness had just closed come back as a reset — an
98
+
unproven outcome for a call that never ran. A proxy pooling connections to the
99
+
harness needs an idle timeout under 120 s for the same reason; Envoy's default
100
+
of one hour is not. Under the connection bound, the harness closing an idle
101
+
connection to make room has the same cost, which is why that bound sits beside
102
+
every reader and every running call rather than in place of them.
64
103
65
104
| Route | Answers |
66
105
| --- | --- |
@@ -117,8 +156,8 @@ as proof would stop looking for a receipt that exists.
0 commit comments