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 READY_HEADER = 'X-APEX-Ready-Token';
|
||||||
const PROBE_TIMEOUT_MS = 2000;
|
const PROBE_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
function hostname(value) {
|
function hostname(value) {
|
||||||
if (typeof value !== 'string' || value.length > 253 || !value.includes('.')) return null;
|
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);
|
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'] });
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||||
let finishPrimary;
|
let finishPrimary;
|
||||||
let canceled = false;
|
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'));
|
: count === 2 ? ready() : new Response('secondary'));
|
||||||
const pending = worker.fetch(new Request(url), env);
|
const pending = worker.fetch(new Request(url), env);
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
t.mock.timers.tick(1999);
|
t.mock.timers.tick(4999);
|
||||||
assert.equal(calls.length, 1);
|
assert.equal(calls.length, 1);
|
||||||
|
assert.equal(calls[0].options.signal.aborted, false);
|
||||||
t.mock.timers.tick(1);
|
t.mock.timers.tick(1);
|
||||||
const response = await pending;
|
const response = await pending;
|
||||||
assert.equal(await response.text(), 'secondary');
|
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);
|
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'] });
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||||
const calls = intercept(t, () => new Promise(() => {}));
|
const calls = intercept(t, () => new Promise(() => {}));
|
||||||
const pending = worker.fetch(new Request(url), env);
|
const pending = worker.fetch(new Request(url), env);
|
||||||
t.mock.timers.tick(2000);
|
t.mock.timers.tick(5000);
|
||||||
await new Promise((resolve) => setImmediate(resolve));
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
assert.equal(calls.length, 2);
|
assert.equal(calls.length, 2);
|
||||||
assert.equal(calls[0].options.signal.aborted, true);
|
assert.equal(calls[0].options.signal.aborted, true);
|
||||||
assert.equal(calls[1].options.signal.aborted, false);
|
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;
|
const response = await pending;
|
||||||
assert.equal(response.status, 503);
|
assert.equal(response.status, 503);
|
||||||
assert.equal(calls[1].options.signal.aborted, true);
|
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.
|
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.
|
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')
|
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]):
|
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')
|
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')
|
raise ValueError('Invalid probe bounds')
|
||||||
if not isinstance(single_flight, bool):
|
if not isinstance(single_flight, bool):
|
||||||
raise ValueError('single_flight must be a boolean')
|
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
|
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):
|
def test_authenticated_success_runs_fixed_probe(self):
|
||||||
self.assertEqual(self.request(self.server()),(204,'no-store',b''));self.assertTrue(self.marker.exists())
|
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):
|
def test_absent_wrong_and_duplicate_tokens_never_probe(self):
|
||||||
s=self.server()
|
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)
|
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