fix(readiness): allow measured native probe execution time
This commit is contained in:
1 parent
31938681dc
commit
e4308a2dd4
5 files changed
+18
-8
No files matched your search
@@ -1,5 +1,5 @@
|
||||
const READY_HEADER = 'X-APEX-Ready-Token';
|
||||
const PROBE_TIMEOUT_MS = 2000;
|
||||
const PROBE_TIMEOUT_MS = 5000;
|
||||
|
||||
function hostname(value) {
|
||||
if (typeof value !== 'string' || value.length > 253 || !value.includes('.')) return null;
|
||||
|
||||
@@ -75,7 +75,7 @@ test('unready primary cancels unread body and selects ready secondary', async (t
|
||||
assert.equal(calls[2].options.cf.resolveOverride, env.APEX_ORIGIN_SECONDARY);
|
||||
});
|
||||
|
||||
test('probe deadline aborts at two seconds even if fetch ignores cancellation', async (t) => {
|
||||
test('probe deadline aborts at five seconds even if fetch ignores cancellation', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
let finishPrimary;
|
||||
let canceled = false;
|
||||
@@ -84,8 +84,9 @@ test('probe deadline aborts at two seconds even if fetch ignores cancellation',
|
||||
: count === 2 ? ready() : new Response('secondary'));
|
||||
const pending = worker.fetch(new Request(url), env);
|
||||
await Promise.resolve();
|
||||
t.mock.timers.tick(1999);
|
||||
t.mock.timers.tick(4999);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].options.signal.aborted, false);
|
||||
t.mock.timers.tick(1);
|
||||
const response = await pending;
|
||||
assert.equal(await response.text(), 'secondary');
|
||||
@@ -227,16 +228,18 @@ test('failed readiness fetch selects secondary without exposing exception detail
|
||||
assert.equal(calls.length, 3);
|
||||
});
|
||||
|
||||
test('secondary probe has its own two-second deadline and no client dispatch follows', async (t) => {
|
||||
test('secondary probe has its own five-second deadline and no client dispatch follows', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const calls = intercept(t, () => new Promise(() => {}));
|
||||
const pending = worker.fetch(new Request(url), env);
|
||||
t.mock.timers.tick(2000);
|
||||
t.mock.timers.tick(5000);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(calls[0].options.signal.aborted, true);
|
||||
assert.equal(calls[1].options.signal.aborted, false);
|
||||
t.mock.timers.tick(2000);
|
||||
t.mock.timers.tick(4999);
|
||||
assert.equal(calls[1].options.signal.aborted, false);
|
||||
t.mock.timers.tick(1);
|
||||
const response = await pending;
|
||||
assert.equal(response.status, 503);
|
||||
assert.equal(calls[1].options.signal.aborted, true);
|
||||
|
||||
@@ -21,7 +21,7 @@ Run `python3 adapter.py --config /private/adapter.json` inside the application f
|
||||
|
||||
Only GET at the exact canonical path, with one matching `X-APEX-Ready-Token` header, can execute the fixed command. Missing/wrong/duplicate credentials return403; other paths404; supported non-GET methods405. Ready returns204; probe failure, timeout or concurrency exhaustion returns503. These responses have empty bodies and `Cache-Control: no-store`. The adapter does not log requests, tokens or command output, cache success, or accept a client-supplied command. Each connection closes after its response.
|
||||
|
||||
Probe timeout is bounded to1.5seconds so an origin can answer within the Worker's two-second deadline. A timeout kills the probe's process group. Probe slots and request threads are bounded; slow socket operations time out after two seconds. Operators must account for proxy overhead and actual probe execution time. Probe programs must not detach into a new session or launch background jobs.
|
||||
Probe timeout defaults to1.5seconds and can be configured up to3seconds. With a3-second probe, allow4seconds at the readiness proxy and5seconds at the Worker. A timeout kills the probe's process group. Probe slots and request threads are bounded; slow socket operations time out after two seconds. Operators must account for proxy overhead and actual probe execution time. Probe programs must not detach into a new session or launch background jobs.
|
||||
|
||||
Optional `single_flight: true` lets overlapping authenticated requests share one running fixed-command probe. It accepts only a JSON boolean and defaults to false. Waiters use that probe's remaining monotonic deadline; they never queue another probe for the same request. The existing connection limit (`slots * 2 + 4`) also bounds waiting requests. Timeout or shutdown returns unavailable and terminates the probe group. Completed results are not cached: each subsequent request starts a fresh probe, with no success TTL or failure cache. Coalescing reduces concurrent work but does not make a slow individual probe meet its deadline.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class ReadinessServer(http.server.ThreadingHTTPServer):
|
||||
raise ValueError('A canonical absolute readiness path is required')
|
||||
if not isinstance(command, list) or not command or not all(isinstance(arg, str) and '\0' not in arg for arg in command) or not os.path.isabs(command[0]):
|
||||
raise ValueError('A fixed absolute executable and argv are required')
|
||||
if isinstance(timeout, bool) or not 0 < timeout <= 1.5 or isinstance(slots, bool) or not isinstance(slots, int) or not 1 <= slots <= 16:
|
||||
if isinstance(timeout, bool) or not 0 < timeout <= 3 or isinstance(slots, bool) or not isinstance(slots, int) or not 1 <= slots <= 16:
|
||||
raise ValueError('Invalid probe bounds')
|
||||
if not isinstance(single_flight, bool):
|
||||
raise ValueError('single_flight must be a boolean')
|
||||
|
||||
@@ -17,6 +17,13 @@ class AdapterTests(unittest.TestCase):
|
||||
c.endheaders();r=c.getresponse();out=(r.status,r.getheader('Cache-Control'),r.read());c.close();return out
|
||||
def test_authenticated_success_runs_fixed_probe(self):
|
||||
self.assertEqual(self.request(self.server()),(204,'no-store',b''));self.assertTrue(self.marker.exists())
|
||||
def test_configured_three_seconds_allows_a_probe_longer_than_default(self):
|
||||
code=f"import time;from pathlib import Path;time.sleep(1.6);Path({str(self.marker)!r}).write_text('completed')"
|
||||
s=self.server(code,timeout=3,single_flight=True)
|
||||
self.assertEqual(self.request(s),(204,'no-store',b''));self.assertEqual(self.marker.read_text(),'completed')
|
||||
def test_timeout_over_three_seconds_is_rejected_before_probe(self):
|
||||
with self.assertRaises(ValueError):self.server(timeout=3.01)
|
||||
self.assertFalse(self.marker.exists())
|
||||
def test_absent_wrong_and_duplicate_tokens_never_probe(self):
|
||||
s=self.server()
|
||||
for token,duplicate in [(None,False),('bad',False),('private-test-token',True)]:self.assertEqual(self.request(s,token=token,extra_token=duplicate)[0],403)
|
||||
|
||||
Reference in new issue
Block a user