summaryrefslogtreecommitdiff
path: root/docs/developer/server/time.md
blob: 2daf4e415861cf286c5f69a3f90515887406c1f6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Time

Handling time in a service is always tricky. Pilcrow splits the problem into two parts: determining a single, consistent timeline for events, and determining what wall time a given operation happened at.

## Sequence numbers

The Pilcrow service tracks [events](data-models.md) in sequence. To accomplish this, the server assigns unique _sequence numbers_ to each event as it happens. Events are always considered in sequential order.

Sequence number assignment is mediated through the `event_sequence` table in Pilcrow's database. Sequence number assignment must happen strictly serially, so this is a potential capacity and concurrency risk to anything that generates new events. In testing, this did not create any practical problems, but it likely contributes to Pilcrow's ultimate throughput limits.

Sequence numbers are little more than 64-bit integers under the hood. The server is permitted to work with that representation directly, but we make no such promise in the API and expect clients to treat them as opaque values wherever they appear. Sequence numbers were at one point a more complex data structure, for example, and no client changes were made to accommodate the switch to serial numbers. If we move away from serial numbers to something else, we intend to follow the same rule.

The server can record slightly more than 9.2 quintillion events before its sequence numbers will be exhausted. To put the magnitude of this range into perspective, a service processing 1k events per second - a rate far higher than Pilcrow is designed for - would run out of sequence numbers after a bit less than 300 million years.

## Wall time

Pilcrow takes the approach that a request is considered to be serviced at one specific point time, and that that time is determined when the request is received, regardless of how long it takes for that request to settle.

The "current time" for a request is determined via an Axum middleware defined in `src/clock.rs`. The resulting time is available to handlers via the `RequestedAt` extractor. This time is always given in UTC, regardless of the time zone the server or client is running in. We expect administrators to keep accurate, or at least consistent, time; NTP is a good solution for this, as it's deployed by default on most popular server operating systems.

Everything on the server that operates on time accepts the "current" time as a parameter. We consistently avoid making any other calls out to clocks, whether in Rust or through SQL constructs like
`NOW()`.

One of the perks of this approach is that tests, which call extractors, app interfaces, and data access interfaces directly, can pass any time they please, rather than being dependent on an external clock. This enables testing scenarios like "if I sent a message last year, it should have expired by today." It also minimizes the risk of chronological anomalies due to clock changes mid-request, whether due to the normal passage of time or otherwise.

## Warning

**Pilcrow does not require that events happen in chronological order.**

Requests that arrive close together may be assigned timestamps in one order, and assigned event sequence numbers in some other order, depending on the vagaries of how the concurrent requests are scheduled. Code that needs to reason about ordering _must not_ use wall time to determine order.