Skip to content

Commit 57f4423

Browse files
author
Guy Bedford
authored
Starlingmonkey timers (#798)
1 parent 4e5b9ee commit 57f4423

File tree

7 files changed

+123
-19
lines changed

7 files changed

+123
-19
lines changed

integration-tests/js-compute/fixtures/app/src/cache-core.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1990,7 +1990,7 @@ let error;
19901990
if (error) { return error }
19911991
await sleep(1000);
19921992
result = CoreCache.lookup(key).age()
1993-
error = assert(result >= 1_000, true, `CoreCache.lookup(key).age() >= 1_000`)
1993+
error = assert(result >= 1_000, true, `CoreCache.lookup(key).age() >= 1_000 (${result})`)
19941994
if (error) { return error }
19951995
return pass("ok")
19961996
});
@@ -2805,7 +2805,7 @@ let error;
28052805
writer.append("hello");
28062806
writer.close();
28072807
const actual = await new Response(reader.body()).text();
2808-
let error = assert("hello", actual, `actual === "hello"`);
2808+
let error = assert(actual, "hello", `actual === "hello"`);
28092809
if (error) { return error }
28102810
return pass("ok")
28112811
});

integration-tests/js-compute/fixtures/app/src/timers.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,24 @@ import { routes } from "./routes.js";
326326
if (error) { return error }
327327
return pass()
328328
});
329+
routes.set("/setTimeout/200-ms", async () => {
330+
let controller, start
331+
setTimeout(() => {
332+
const end = Date.now()
333+
controller.enqueue(new TextEncoder().encode(`END\n`))
334+
if (end - start < 190) {
335+
controller.enqueue(new TextEncoder().encode(`ERROR: Timer took ${end - start} instead of 200ms`))
336+
}
337+
controller.close()
338+
}, 200);
339+
return new Response(new ReadableStream({
340+
start(_controller) {
341+
controller = _controller
342+
start = Date.now()
343+
controller.enqueue(new TextEncoder().encode(`START\n`))
344+
}
345+
}))
346+
});
329347
}
330348

331349
// clearInterval
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
[
2-
"GET /cache-entry/age/called-on-instance",
32
"GET /transaction-cache-entry/insertAndStreamBack/write-to-writer-and-read-from-reader"
43
]

integration-tests/js-compute/fixtures/app/tests.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4834,6 +4834,17 @@
48344834
"status": 200
48354835
}
48364836
},
4837+
"GET /setTimeout/200-ms": {
4838+
"environments": ["viceroy", "compute"],
4839+
"downstream_request": {
4840+
"method": "GET",
4841+
"pathname": "/setTimeout/200-ms"
4842+
},
4843+
"downstream_response": {
4844+
"status": 200,
4845+
"body": ["START\nEND\n"]
4846+
}
4847+
},
48374848
"GET /clearInterval/exposed-as-global": {
48384849
"environments": ["viceroy", "compute"],
48394850
"downstream_request": {

integration-tests/js-compute/test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ if (!local) {
100100
domain = "http://127.0.0.1:7676"
101101
}
102102

103-
core.startGroup('Check service is up and running')
103+
core.startGroup(`Check service is up and running on ${domain}`)
104104
await retry(10, expBackoff('60s', '30s'), async () => {
105105
const response = await request(domain)
106106
if (response.statusCode !== 200) {

runtime/fastly/host-api/host_api.cpp

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,102 @@
1010
#include <algorithm>
1111
#include <arpa/inet.h>
1212

13+
#include <time.h>
14+
1315
using api::FastlyResult;
1416
using fastly::FastlyAPIError;
17+
using host_api::MonotonicClock;
1518
using host_api::Result;
1619

20+
#define NEVER_HANDLE 0xFFFFFFFE
21+
22+
#define MILLISECS_IN_NANOSECS 1000000
23+
#define SECS_IN_NANOSECS 1000000000
24+
25+
void sleep_until(uint64_t time_ns, uint64_t now) {
26+
while (time_ns > now) {
27+
uint64_t duration = time_ns - now;
28+
timespec req{.tv_sec = static_cast<time_t>(duration / SECS_IN_NANOSECS),
29+
.tv_nsec = static_cast<long>(duration % SECS_IN_NANOSECS)};
30+
timespec rem;
31+
nanosleep(&req, &rem);
32+
now = MonotonicClock::now();
33+
}
34+
}
35+
1736
size_t api::AsyncTask::select(std::vector<api::AsyncTask *> *tasks) {
1837
size_t tasks_len = tasks->size();
19-
fastly_compute_at_edge_async_io_handle_t *handles =
20-
new fastly_compute_at_edge_async_io_handle_t[tasks_len];
21-
for (int i = 0; i < tasks_len; i++) {
22-
handles[i] = tasks->at(i)->id();
38+
std::vector<fastly_compute_at_edge_async_io_handle_t> handles;
39+
handles.reserve(tasks_len);
40+
uint64_t now = 0;
41+
uint64_t soonest_deadline = 0;
42+
size_t soonest_deadline_idx = -1;
43+
for (size_t idx = 0; idx < tasks_len; ++idx) {
44+
auto *task = tasks->at(idx);
45+
uint64_t deadline = task->deadline();
46+
// Select for completed task deadlines before performing the task select host call.
47+
if (deadline > 0) {
48+
MOZ_ASSERT(task->id() == NEVER_HANDLE);
49+
if (now == 0) {
50+
now = MonotonicClock::now();
51+
MOZ_ASSERT(now > 0);
52+
}
53+
if (deadline <= now) {
54+
return idx;
55+
}
56+
if (soonest_deadline == 0 || deadline < soonest_deadline) {
57+
soonest_deadline = deadline;
58+
soonest_deadline_idx = idx;
59+
}
60+
} else {
61+
uint32_t handle = task->id();
62+
// Timer task handles are skipped and never passed to the host.
63+
MOZ_ASSERT(handle != NEVER_HANDLE);
64+
handles.push_back(handle);
65+
}
2366
}
24-
fastly_world_list_handle_t hs{.ptr = handles, .len = tasks_len};
67+
68+
// When there are no async tasks, sleep until the deadline
69+
if (handles.size() == 0) {
70+
MOZ_ASSERT(soonest_deadline > 0);
71+
sleep_until(soonest_deadline, now);
72+
return soonest_deadline_idx;
73+
}
74+
75+
fastly_world_list_handle_t hs{.ptr = handles.data(), .len = handles.size()};
2576
fastly_world_option_u32_t ret;
2677
fastly_compute_at_edge_types_error_t err = 0;
27-
if (!fastly_compute_at_edge_async_io_select(&hs, 0, &ret, &err)) {
28-
abort();
29-
} else if (ret.is_some) {
30-
return ret.val;
31-
} else {
32-
abort();
78+
79+
while (true) {
80+
if (!fastly_compute_at_edge_async_io_select(
81+
&hs, (soonest_deadline - now) / MILLISECS_IN_NANOSECS, &ret, &err)) {
82+
abort();
83+
} else if (ret.is_some) {
84+
// The host index will be the index in the list of tasks with the timer tasks filtered out.
85+
// We thus need to offset the host index by any timer tasks appearing before the nth
86+
// non-timer task.
87+
size_t task_idx = 0;
88+
for (size_t idx = 0; idx < tasks_len; ++idx) {
89+
if (tasks->at(idx)->id() != NEVER_HANDLE) {
90+
if (ret.val == task_idx) {
91+
return idx;
92+
}
93+
task_idx++;
94+
}
95+
}
96+
abort();
97+
} else {
98+
// No value case means a timeout, which means soonest_deadline_idx is set.
99+
MOZ_ASSERT(soonest_deadline > 0);
100+
MOZ_ASSERT(soonest_deadline_idx != -1);
101+
// Verify that the task definitely is ready from a time perspective, and if not loop the host
102+
// call again.
103+
now = MonotonicClock::now();
104+
if (soonest_deadline > now) {
105+
continue;
106+
}
107+
return soonest_deadline_idx;
108+
}
33109
}
34110
}
35111

@@ -96,11 +172,11 @@ Result<uint32_t> Random::get_u32() {
96172
return res;
97173
}
98174

99-
uint64_t MonotonicClock::now() { return 0; }
175+
uint64_t MonotonicClock::now() { return JS_Now() * 1000; }
100176

101-
uint64_t MonotonicClock::resolution() { return 1000000; }
177+
uint64_t MonotonicClock::resolution() { return 1000; }
102178

103-
int32_t MonotonicClock::subscribe(const uint64_t when, const bool absolute) { return 0; }
179+
int32_t MonotonicClock::subscribe(const uint64_t when, const bool absolute) { return NEVER_HANDLE; }
104180

105181
void MonotonicClock::unsubscribe(const int32_t handle_id) {}
106182

0 commit comments

Comments
 (0)