How polls and max_concurrent: run
This is the detail behind Polls and a connection's
max_concurrent:: what a poll declares, when it runs and what it keeps,
and how the cap on calls at once is counted.
- A poll's keys. Each entry of
polls:inprovider.jadpois a block ofcall:,every:andemits:, each required and nothing else (package.provider_invalid).call:names one of the package's operations, public or not, that takes noinput:and isn'tchanges: true, since a poll reads a feed that doesn't depend on the app's data (checking each of the app's records is the app's schedule calling the package), and whoseoutput:is anObjectread by the package's types, with nodecode:of its own, as a queue's mapping is.every:is a duration of a minute or more: the tick runs once a minute. - What it emits.
emits:names anEventof the package that ispublic, since only the app's subscribers hear it, and gives nowire_name:, which marks one the provider sends; it is the package's own, as one its action emits is, so a package that receives webhooks needs nowire_name:oroccurred_aton it. Each of its fields is a field of the answer of the same name and type, and only those count when the runtime asks whether the answer changed: a time the answer was fetched, left out of the event, never does. An event of fields the answer hasn't is refused atemits:. - The connection's interval.
polls: { <name>: <interval> }on a connection gives one of its package's polls another interval, a duration of a minute or more; a name the package'spolls:hasn't isconnection.key_invalidwith the nearest, a shorter or unwritten intervalconnection.value_invalid, andpolls:on a connection to a package with noneconnection.key_invalid. - Only while subscribed. A poll runs for each connection to its
package while one of the app's subscribers listens for its event, and
is left out of the plan otherwise, so nothing is called and no state
kept. Its events are kept where the package's webhooks would be, in
events: { store: }or the only store of the app's records (event.store_requiredat the connection otherwise), withjadpo_polls, bypoll <connection>.<poll>: when it is next due, the digest of the event it last kept and when that changed. - Running it. On each tick, after the queues, one statement takes a
poll that is due, or has never run, and moves it on by its interval, so
of several instances only one calls it; one due later than a whole new
interval away, since shortened, is due at once. The row keeps the
interval it was last claimed under,
jadpo_polls.every_ms, so only a poll claimed under another interval than the connection's now counts as shortened, and a second tick at the same moment, or an instance whose clock is behind, never calls the provider again within the interval. The column may beNULL, since a migration adds it to a table that may hold rows, and a row without it counts once as claimed under another interval. The operation is called as the runtime's own work, through the connection, with no request and no function of the package's, logged and retried as any call is. The event is made from the answer's fields, written with its codec and read back, and the SHA-256 of what is written compared with the last digest: the same keeps nothing; a change, or the first answer, keeps the event with a delivery for each subscriber and the digest in one transaction, the digest checked again there. A failed call keeps nothing, is reported by the tick under the poll's name, and is due again a minute later. A poll's first answer is emitted, so the app learns the current prices without waiting for them to change. max_concurrent:inprovider.jadpois a whole number of at least 1 (package.provider_invalid), and a connection's may only lower it (connection.value_invalidpast it); a connection may give one when its package gives none. The cap is per app instance: the process's, shared by its worker threads, or an isolate's on Workers, and a test's host keeps its own. A provider call takes one of its connection's slots for each attempt and gives it back once answered, so none is held while a retry waits; a further call waits for one, a route's within its deadline. A subscriber's delivery calling out through a connection whose slots are all out is given back as claimed, its attempt not counted, due again a second later for the next sweep; its wait counts towardswithin:, as a release after a shutdown does. Polls' calls wait as any call does.- On Workers the cap is counted per isolate: Workers runs many
isolates, and a cap across them would need a Durable Object and a round
trip per call. So the deploy report and
jadpo checksay "on Workers, per instance" (jadpo check's "On Workers" lists each capped connection a deployable built for Workers reaches, andjadpo build'scallsline each Worker's); a provider that needs a hard global limit enforces it on its side, and its rate-limit answers already make calls back off. - Between an isolate's invocations a slot one invocation gives back is handed to the call of another that has waited longest, which finds it on its own timer every 10 ms rather than being woken by the first, since Workers runs each invocation's work in its own context. A call holding a slot says so every second, and one not heard from for five seconds is forgotten, as is a waiter that stopped looking, so an invocation Workers ended mid-call never keeps a slot. Natively the process's waiters are woken by the call that gives a slot back.
- The surface. A poll's event is one of the package's public types
and in its surface as any is; the polls themselves and
max_concurrent:join the provider's entry once the surface tells an addition inside an entry from a breaking change, since any change to the provider's entry counts as breaking.
Markdown for agents: /docs/how-polls-and-max-concurrent-run.md.Built from Jadpo main at a707efb.