@mark1russell7/service 0.5.12 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rabbitmq.js","names":["knob","t"],"sources":["../src/rabbitmq/connection.ts","../src/rabbitmq/meets.ts","../src/rabbitmq/client.ts","../src/rabbitmq/machine.ts","../src/rabbitmq/starting.ts"],"sourcesContent":["import type { ClientSocket, Factory } from \"../client/core.ts\";\n\n/** A message is a message of the broker, as amqplib gives it from `get`. The wrapper reads only these fields. */\nexport interface RabbitMessage {\n readonly content: Buffer;\n readonly fields: {\n readonly deliveryTag: number;\n readonly redelivered: boolean;\n readonly exchange: string;\n readonly routingKey: string;\n /** This count is the number of messages that stay on the queue after this one. */\n readonly messageCount?: number | undefined;\n };\n readonly properties: { readonly headers?: Readonly<Record<string, unknown>> | undefined; readonly [property: string]: unknown };\n}\n\n/** These are the properties of a publish that amqplib takes, for example `persistent`, `messageId` and `headers`. */\nexport type RabbitPublishOptions = Readonly<Record<string, unknown>>;\n\n/** These are the events of a channel that the wrapper listens to. */\nexport type RabbitChannelEvent = \"error\" | \"close\";\n\n/**\n * A channel is the part of a `ConfirmChannel` of amqplib 2 that the wrapper uses. A confirm channel\n * gives one result to the callback of each publish. The result is null for a `basic.ack`. It is an\n * error for a `basic.nack` and for the end of the channel.\n */\nexport interface RabbitChannel {\n /** This method writes one message. It throws when the channel is closed or closing. */\n publish(exchange: string, routingKey: string, content: Buffer, options: RabbitPublishOptions, callback: (error: Error | null) => void): boolean;\n /** This method takes one message (`basic.get`), or gives false when the queue is empty. It has no timer of its own. */\n get(queue: string, options?: { readonly noAck?: boolean | undefined }): Promise<RabbitMessage | false>;\n /** This method declares an exchange in passive mode: an RPC that changes nothing. The round-trip check uses it. */\n checkExchange(exchange: string): Promise<unknown>;\n /** This method acknowledges one message. It is one frame, with no reply. */\n ack(message: RabbitMessage): void;\n /** This method gives one message back to the broker, to the queue or to its dead-letter route. It is one frame, with no reply. */\n nack(message: RabbitMessage, allUpTo?: boolean, requeue?: boolean): void;\n close(): Promise<void>;\n on(event: RabbitChannelEvent, listener: (error?: Error) => void): unknown;\n off(event: RabbitChannelEvent, listener: (error?: Error) => void): unknown;\n}\n\n/** These are the events of a connection that the wrapper listens to. The event `blocked` gives the reason of the broker. */\nexport type RabbitConnectionEvent = \"error\" | \"close\" | \"blocked\" | \"unblocked\";\n\n/**\n * A connection is the part of a `ChannelModel` of amqplib 2 that the wrapper uses. A `ChannelModel`\n * has each part. A fake for tests has them too. Thus the package depends on no type of amqplib.\n */\nexport interface RabbitConnection {\n /** This method opens a channel with confirms (`confirm.select`). */\n createConfirmChannel(): Promise<RabbitChannel>;\n /** This method sends `connection.close`, and waits for `connection.close-ok`. Behind a black hole, it waits with no bound. */\n close(): Promise<void>;\n on(event: RabbitConnectionEvent, listener: (detail?: unknown) => void): unknown;\n off(event: RabbitConnectionEvent, listener: (detail?: unknown) => void): unknown;\n /** This field holds the socket. The join reads its addresses, and the bounded close destroys it. */\n readonly connection: { readonly stream?: ClientSocket | undefined };\n}\n\n/**\n * These are the socket options that the wrapper gives amqplib. Each one comes from the declaration.\n * At its defaults, amqplib sets no connect timeout, no keepalive and no TCP_NODELAY.\n */\nexport interface RabbitSocketOptions {\n /** This time bounds the TCP connect and the handshake. The timer stops when the connection opens. A time of 0 is no bound. */\n readonly timeout: number;\n /** The library turns Nagle off only when the caller asks. A confirm and a reply then do not wait for a delayed ACK. */\n readonly noDelay: true;\n readonly keepAlive: boolean;\n /** This time is the idle time of TCP keepalive, in milliseconds. Node truncates it to whole seconds. */\n readonly keepAliveDelay?: number | undefined;\n /** The broker shows `connection_name` in its management view and its log, thus the prober can join on it too. */\n readonly clientProperties: { readonly connection_name: string };\n}\n\n/** These are the arguments of `connect(url, socketOptions)`. The URL gives the host, the vhost, the user and the heartbeat. */\nexport interface RabbitConnectOptions {\n readonly url: string;\n readonly socketOptions: RabbitSocketOptions;\n}\n\n/** A connection factory makes one connection of amqplib from its options. A failure to open rejects. */\nexport type RabbitFactory = Factory<RabbitConnectOptions, RabbitConnection>;\n\n/**\n * This factory opens a connection of amqplib. It loads amqplib only when a client connects, thus a\n * service that does not use RabbitMQ does not need it. The specifier is a value, thus the package\n * needs no types of amqplib. The built-in recovery of amqplib stays off, because the wrapper connects again by itself.\n */\nexport const nodeAmqp: RabbitFactory = async ({ url, socketOptions }) => {\n const specifier = \"amqplib\";\n type Connect = (url: string, options: RabbitSocketOptions) => Promise<RabbitConnection>;\n const loaded = (await import(specifier)) as { readonly connect?: Connect; readonly default?: { readonly connect: Connect } };\n const connect = loaded.connect ?? loaded.default?.connect;\n if (connect === undefined) throw new Error(\"amqplib gives no connect function\");\n return connect(url, socketOptions);\n};\n","import type { Extent } from \"@mark1russell7/quantity\";\nimport { quantity } from \"@mark1russell7/quantity\";\nimport type { Detector, Meeting, Situation } from \"../client/meets.ts\";\nimport { NEVER, WHAT, immediate, keepaliveAnswered, keepaliveReset, keepaliveSilent, kernelGiveUp } from \"../client/meets.ts\";\nimport { finiteMs } from \"../client/core.ts\";\nimport type { RabbitKnobs } from \"./client.ts\";\n\n/**\n * This function gives the manifestations that a RabbitMQ client can meet, and the detectors of each,\n * from its knobs. The cells of R3 measured them on amqplib 2.2.0 against RabbitMQ 4.3.6:\n * - The heartbeat ends a silent connection 2 to 3 heartbeats after the last data. The cells measured\n * 180.009 s at 60 s, and 45.009 s at 15 s (O17). It finds a silent broker in flight and idle.\n * - A stopped broker resets each client at once, because each socket of RabbitMQ has linger 0.\n * - At `tcp_retries2 = 7`, the kernel gave up at 53.016 s.\n * - A broker with a resource alarm blocks each connection that publishes, and it still sends its heartbeats.\n */\nexport const rabbitMeetings = (name: string, knobs: RabbitKnobs): Meeting[] => {\n const beat = finiteMs(knobs.heartbeat_interval);\n const detection: Extent = beat === undefined ? NEVER : quantity((3 * beat) / 1000, \"s\");\n const heartbeat = (situation: Situation): Detector => ({\n by: \"heartbeat_detection_time\",\n within: detection,\n situation,\n effect: `the heartbeat of amqplib ends the connection, each call in flight fails as indeterminate, and ${name} connects again`,\n });\n const call: Detector = {\n by: \"request_timeout\",\n within: knobs.request_timeout,\n situation: \"in flight\",\n effect: \"the timer of the wrapper ends the call as indeterminate, and the connection stays\",\n };\n const giveUp = kernelGiveUp(knobs.retrans_giveup_time, \"each call in flight fails as indeterminate\");\n const silent = (manifestation: \"blackhole\" | \"freeze-host\"): Meeting => ({\n manifestation,\n what: WHAT[manifestation],\n detectors: [call, heartbeat(\"in flight\"), giveUp, heartbeat(\"idle\"), keepaliveSilent(knobs.keepalive_idle)],\n });\n return [\n {\n manifestation: \"rst\",\n what: WHAT.rst,\n detectors: [\n immediate(\"in flight\", \"the call fails with ECONNRESET as indeterminate, and the client connects again\"),\n {\n by: \"heartbeat_interval\",\n within: beat === undefined ? NEVER : knobs.heartbeat_interval,\n situation: \"idle\",\n effect: \"the next heartbeat frame of the client gets a reset, and the client connects again\",\n },\n keepaliveReset(knobs.keepalive_idle),\n ],\n },\n {\n manifestation: \"fin\",\n what: WHAT.fin,\n detectors: [\n immediate(\"in flight\", \"the broker closes its socket with linger 0, thus a reset arrives, and the call fails as indeterminate\"),\n immediate(\"idle\", \"the reset of the broker arrives, and the client connects again\"),\n ],\n },\n silent(\"blackhole\"),\n silent(\"freeze-host\"),\n {\n manifestation: \"freeze-process\",\n what: WHAT[\"freeze-process\"],\n detectors: [call, heartbeat(\"in flight\"), heartbeat(\"idle\"), keepaliveAnswered()],\n },\n {\n manifestation: \"app-stall\",\n what: \"the broker stops its confirms and its replies for a time, and still sends its heartbeats: a resource alarm, or a quorum queue with no leader\",\n detectors: [\n call,\n {\n by: \"connection.blocked\",\n within: quantity(0, \"s\"),\n situation: \"idle\",\n effect: \"a resource alarm blocks a connection that publishes at once. The client then refuses each call as not sent, until the unblock\",\n },\n { ...heartbeat(\"idle\"), within: NEVER, effect: \"the broker still sends its heartbeats\" },\n keepaliveAnswered(),\n ],\n },\n ];\n};\n","import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { quantity } from \"@mark1russell7/quantity\";\nimport { realClock } from \"../clock.ts\";\nimport type { Attempt, CheckOptions, Client, ClientJoin, ClientKnobs, ClientOptions, ClientSpec, ClientStep, DialFn, RetryKnobs, SendOptions, Sent } from \"../client/core.ts\";\nimport {\n ageProblems,\n attachClient,\n clientBounds,\n clientProblems,\n clientRecorder,\n dialCheck,\n finiteMs,\n inFlight,\n localStateCheck,\n message,\n ms,\n notSent,\n operationTable,\n readyWait,\n retryBackoff,\n retryProblems,\n retrySchedule,\n roundTripCheck,\n sendWithReissue,\n socketEnds,\n stepChecks,\n timeProblems,\n trackWithTimer,\n wholeProblems,\n withinBound,\n wrapperTimerProblems,\n zeroProblems,\n} from \"../client/core.ts\";\nimport type { Judge } from \"../client/dial.ts\";\nimport { helloDial } from \"../client/dial.ts\";\nimport type { Dependency } from \"../description.ts\";\nimport type { DependencyHandle } from \"../lifecycle.ts\";\nimport type { JobSettle } from \"../queue.ts\";\nimport type { RabbitChannel, RabbitConnectOptions, RabbitConnection, RabbitFactory, RabbitMessage, RabbitPublishOptions } from \"./connection.ts\";\nimport { nodeAmqp } from \"./connection.ts\";\nimport { rabbitMeetings } from \"./meets.ts\";\n\n/**\n * These are the knobs of the client, each a time, by the names of the ordering law where the law\n * has them. The runtime has no defaults, thus each knob is necessary. A bound that nobody gives is\n * \"infinite\", not zero. The constant `RABBIT_STARTING_KNOBS` gives a declaration from the cells of R3. The knobs\n * that each client has come from the core.\n */\nexport interface RabbitKnobs extends ClientKnobs, RetryKnobs {\n /**\n * This time bounds the TCP connect and the handshake (`socketOptions.timeout`). The client also\n * bounds the open of its channel with it. The timer of amqplib stops when the connection opens.\n */\n readonly connect_bound: Quantity;\n /** This time bounds each call: a publish until its confirm, and an RPC until its reply. The wrapper has this timer, because amqplib has none. */\n readonly request_timeout: Quantity;\n /**\n * This time is the heartbeat that the client asks for (`heartbeat` in the URL), in whole seconds.\n * The connection uses the smaller of this value and the value of the broker. The value \"infinite\" asks for no heartbeat.\n */\n readonly heartbeat_interval: Extent;\n /** This time is the first delay before a new try to connect. The delay doubles after each try. */\n readonly retry_base: Quantity;\n /** This time is the most that one delay can be, before its jitter. */\n readonly retry_cap: Quantity;\n /** This time is the idle time of TCP keepalive (`keepAliveDelay`), in whole seconds. The value \"infinite\" turns keepalive off, as amqplib does by default. */\n readonly keepalive_idle: Extent;\n /** The client does not replace its connection at an age, thus this knob must be \"infinite\". */\n readonly max_connection_age: Extent;\n /** This time bounds the close of the client at the shutdown: `connection.close`, then a destroy of the socket. */\n readonly close_bound: Quantity;\n}\n\n/** A seed is one more node of the cluster. The client connects to the next node after each failure, in sequence. */\nexport interface RabbitSeed {\n readonly host: string;\n readonly port: number;\n}\n\n/** A RabbitMQ client spec is the declaration of one RabbitMQ client: where it connects, and its knobs. */\nexport interface RabbitClientSpec extends ClientSpec<RabbitKnobs> {\n /** This name is the virtual host. The default of the broker is \"/\". */\n readonly vhost?: string | undefined;\n /** This name is the user. Without it, amqplib connects as the user \"guest\". */\n readonly username?: string | undefined;\n /** These nodes are more nodes of the cluster, after `host` and `port`. */\n readonly seeds?: readonly RabbitSeed[] | undefined;\n /**\n * These operations are the calls of the service, by name, with their idempotency. A message does\n * not tell if a consumer removes its duplicates. Thus the client sends again only a call that this table names.\n */\n readonly operations?: ClientSpec<RabbitKnobs>[\"operations\"];\n}\n\n/** These options make a client. Without them, it uses amqplib, the real clock, `Math.random`, and a dial with the protocol header. */\nexport interface RabbitClientOptions extends ClientOptions<RabbitFactory> {\n readonly random?: (() => number) | undefined;\n}\n\n/**\n * A join is one generation of the client: one connection and its channel, when the channel is open\n * (D1, D2). The prober joins its kernel records to the view of the broker through it.\n */\nexport interface RabbitJoin extends ClientJoin {\n /** This text is the node of the connection, as `host:port`. */\n readonly address: string;\n /** This text is the name of the connection in the broker, `local:port -> remote:port`, or null when the socket gives no addresses. */\n readonly connectionName: string | null;\n}\n\n/** A publish is one message to an exchange. A string content goes as UTF-8. */\nexport interface RabbitPublish {\n readonly exchange: string;\n readonly routingKey: string;\n readonly content: Buffer | string;\n /** These properties go to amqplib, for example `persistent`, `messageId` and `headers`. */\n readonly properties?: RabbitPublishOptions | undefined;\n}\n\n/** A delivery is one message that the client took from a queue. It stays on the channel of its generation until a settle. */\nexport interface RabbitDelivery {\n readonly message: RabbitMessage;\n /** This number is the generation of the channel that holds the message. */\n readonly generation: number;\n /** This count is the delivery of the message, 1 at the first, from `x-delivery-count` or `redelivered`. */\n readonly delivery: number;\n}\n\n/** These are the source and the settle of a queue drain on one queue: `queueDrain({ source, settle, ... })`. */\nexport interface RabbitJobs {\n /** This function takes one job, or gives null when the queue is empty. It is a `JobSource` of the drain. */\n readonly source: (signal: AbortSignal) => Promise<RabbitDelivery | null>;\n readonly settle: JobSettle<RabbitDelivery>;\n}\n\n/** A RabbitMQ client is a dependency of a service: its declaration, its handle, its checks and its calls. */\nexport interface RabbitClient extends Client<ClientStep, RabbitJoin> {\n /** This text is the state of the client, as `RABBIT_CLIENT_MACHINE` names it. */\n readonly status: string;\n /**\n * This method publishes one message, and waits for its confirm. An ack is `ok`. The client sends\n * again only an idempotent operation, one time at most, after an outcome that is not certain.\n */\n publish(operation: string, message: RabbitPublish, options?: SendOptions): Promise<Sent<void>>;\n /** This method takes one message from a queue (`basic.get`), or gives null when the queue is empty. */\n get(operation: string, queue: string, options?: SendOptions): Promise<Sent<RabbitDelivery | null>>;\n /** This method gives the source and the settle of a queue drain. The source takes each job with `get`. */\n jobs(operation: string, queue: string): RabbitJobs;\n}\n\n/** This function gives the problems of a spec: the knobs that the client cannot use. */\nexport const specProblems = (name: string, spec: RabbitClientSpec): string[] => {\n const { knobs } = spec;\n return [\n ...timeProblems(name, knobs),\n // amqplib reads a socket timeout of 0 as no bound, and a heartbeat of 0 as no heartbeat\n ...zeroProblems(name, \"amqplib\", knobs, { bounded: [\"connect_bound\"], open: [\"heartbeat_interval\", \"keepalive_idle\"] }),\n // the URL takes the heartbeat in whole seconds, and Node truncates the keepalive delay to whole seconds\n ...wholeProblems(name, knobs, { millis: [\"connect_bound\"], seconds: [\"heartbeat_interval\", \"keepalive_idle\"] }),\n ...retryProblems(name, knobs),\n // amqplib has no timer of a call\n ...wrapperTimerProblems(name, knobs, { request_timeout: \"a call\" }),\n ...ageProblems(name, knobs, \"the client does not replace its connection at an age\"),\n ];\n};\n\n/** This function gives the node of a try: `host`, then each seed, in sequence. */\nconst nodeOf = (spec: RabbitClientSpec, index: number): RabbitSeed => {\n const nodes = [{ host: spec.host, port: spec.port }, ...(spec.seeds ?? [])];\n return nodes[index % nodes.length]!;\n};\n\n/**\n * This function gives the arguments of `connect` for one node of a spec. Each timer of amqplib\n * comes from the declaration. The heartbeat goes in the URL, and \"infinite\" is 0, no heartbeat.\n */\nexport const rabbitOptions = (name: string, spec: RabbitClientSpec, index = 0): RabbitConnectOptions => {\n const { knobs } = spec;\n const node = nodeOf(spec, index);\n const user = spec.username === undefined ? \"\" : `${encodeURIComponent(spec.username)}:${encodeURIComponent(spec.password ?? \"\")}@`;\n const heartbeat = finiteMs(knobs.heartbeat_interval);\n const keepalive = finiteMs(knobs.keepalive_idle);\n return {\n url: `amqp://${user}${node.host}:${node.port}/${encodeURIComponent(spec.vhost ?? \"/\")}?heartbeat=${heartbeat === undefined ? 0 : heartbeat / 1000}`,\n socketOptions: {\n timeout: ms(knobs.connect_bound),\n noDelay: true,\n keepAlive: keepalive !== undefined,\n ...(keepalive === undefined ? {} : { keepAliveDelay: keepalive }),\n clientProperties: { connection_name: name },\n },\n };\n};\n\n/** The bytes of the protocol header of AMQP 0-9-1. A broker answers it with `connection.start`, before the authentication. */\nexport const AMQP_HEADER: Uint8Array = Uint8Array.of(0x41, 0x4d, 0x51, 0x50, 0, 0, 9, 1);\n\n/**\n * This judge reads the answer to the protocol header. A broker that takes the version sends a method\n * frame on channel 0: `connection.start`, class 10 and method 10. A broker that refuses the version\n * sends its own protocol header, and closes the socket.\n */\nexport const startJudge: Judge = (received) => {\n if (received.length < 4) return \"more\";\n if (received.toString(\"latin1\", 0, 4) === \"AMQP\") return new Error(`the broker refused the version: it answered ${received.toString(\"hex\")}`);\n if (received.length < 11) return \"more\";\n const type = received[0];\n const channel = received.readUInt16BE(1);\n const classId = received.readUInt16BE(7);\n const methodId = received.readUInt16BE(9);\n if (type !== 1 || channel !== 0) return new Error(`the answer to the header is a frame of type ${type} on channel ${channel}, not a method on channel 0`);\n return classId === 10 && methodId === 10 ? null : new Error(`the answer to the header is the method ${classId}.${methodId}, not connection.start (10.10)`);\n};\n\n/** This dial function opens a TCP connection, sends the protocol header, and waits for `connection.start`. It needs no password. */\nexport const amqpDial: DialFn = helloDial(AMQP_HEADER, startJudge);\n\n/**\n * This function gives the outcome of a failure of amqplib. The library names few classes of error.\n * Thus the wrapper reads the fields:\n * - A throw before the write (`IllegalOperationError`: the channel is closed or closing) is certain: the call did not leave the client.\n * - A reply code of the broker, from the close of the channel, is certain. The broker refused the call, or a call before it.\n * - A nack, and an end of the channel before the reply or the confirm, are not certain.\n */\nexport const failureOutcome = (error: unknown): Attempt<never> => {\n const text = message(error);\n const { name, code } = (typeof error === \"object\" && error !== null ? error : {}) as { name?: unknown; code?: unknown };\n if (name === \"IllegalOperationError\") return notSent(text);\n if (typeof code === \"number\") return { outcome: \"error\", error: `${code} ${text}`, reply: true };\n if (text === \"message nacked\")\n return { outcome: \"indeterminate\", error: \"the broker nacked the message: a queue of the route can hold it, or not\" };\n return { outcome: \"indeterminate\", error: text };\n};\n\n/** This function gives the count of deliveries of a message, 1 at the first. A quorum queue counts in `x-delivery-count`. */\nexport const deliveryCount = (message: RabbitMessage): number => {\n const count = message.properties.headers?.[\"x-delivery-count\"];\n if (typeof count === \"number\" && Number.isInteger(count) && count >= 0) return count + 1;\n return message.fields.redelivered ? 2 : 1;\n};\n\n/** The exchange of each broker: the round-trip check declares it in passive mode, which changes nothing. */\nconst CHECK_EXCHANGE = \"amq.direct\";\n\n/**\n * This function makes a RabbitMQ client, a dependency of a service over amqplib 2.\n * - It has one connection and one confirm channel, as one generation. The end of either one ends\n * the generation. The client then connects again by itself, to the next node, after a backoff.\n * - It sets each timer of amqplib from the declaration. The built-in recovery of amqplib stays off.\n * - Its call is a confirmed publish, or an RPC with a reply. Each call has a timer of the wrapper, because amqplib has none.\n * - It tells the outcome of a call in three values, and it settles each call in flight at the end of its generation (C8, C10).\n * - It records the node and the name of each connection at the open of its channel (D1).\n *\n * These parts come from the core of the clients: the outcomes, the second try, the wait for a ready client and the backoff.\n * The calls in flight, the bounded close and the checks also come from the core. This entry gives what amqplib does in its own way.\n */\nexport const rabbitClient = (name: string, spec: RabbitClientSpec, options: RabbitClientOptions = {}): RabbitClient => {\n const problems = specProblems(name, spec);\n if (problems.length > 0) throw new Error(problems.join(\"; \"));\n const { knobs } = spec;\n const clock = options.clock ?? realClock;\n const random = options.random ?? Math.random;\n const create = options.create ?? nodeAmqp;\n const dial = options.dial ?? amqpDial;\n const operations = operationTable({}, spec.operations);\n const requestTimeout = ms(knobs.request_timeout);\n const backoff = retryBackoff(knobs);\n\n let status = \"none\";\n let connection: RabbitConnection | null = null;\n let channel: RabbitChannel | null = null;\n // the generation of the channel that is open now, and the token of the open that is current\n let generation = 0;\n let token = 0;\n // the index of the node of the next try, and the tries since the last join\n let next = 0;\n const retries = retrySchedule(clock, backoff, random);\n const joins: RabbitJoin[] = [];\n const flight = inFlight();\n const readied = readyWait(clock, () => status === \"ready\");\n\n const emit = clientRecorder(options.record, clock, name);\n const record = (on: string, to: string = status, detail?: string): void => {\n const from = status;\n status = to;\n emit(from, to, on, detail);\n };\n const closing = (): boolean => status === \"closing\" || status === \"end\";\n\n /** This function closes a connection that the client does not use, in `close_bound`, then destroys its socket. */\n const retire = (c: RabbitConnection): void => {\n void withinBound(\n clock,\n ms(knobs.close_bound),\n Promise.resolve().then(() => c.close()),\n ).then((ended) => {\n if (!ended) c.connection.stream?.destroy?.();\n });\n };\n\n /** This function waits for the backoff of the next try, then opens a new generation on the next node. */\n const scheduleRetry = (): void =>\n retries.schedule(() => {\n if (closing()) return;\n record(\"retry.connect\", \"connecting\", `try ${retries.tries}, ${nodeOf(spec, next).host}:${nodeOf(spec, next).port}`);\n void open();\n });\n\n /** This function ends the generation that is open. The calls in flight learn it after the rejections of amqplib, which can hold a reply code. */\n const lose = (on: string, detail: string): void => {\n const c = connection;\n connection = null;\n channel = null;\n if (closing() || c === null) return;\n record(on, \"failing\", detail);\n clock.after(0, () => flight.settleAll(`the generation ended with the call in flight (${detail})`));\n // the end of the channel leaves its connection open: the client closes it\n if (on === \"channel.end\") retire(c);\n scheduleRetry();\n };\n\n /** This function listens to a connection from its creation. The error listener stays: in Node, an error with no listener throws. */\n const watch = (c: RabbitConnection): void => {\n c.on(\"error\", (error) => {\n if (c === connection) record(\"connection.error\", status, message(error));\n });\n c.on(\"close\", (error) => {\n if (c === connection) lose(\"connection.end\", error === undefined ? \"the connection closed\" : message(error));\n });\n c.on(\"blocked\", (reason) => {\n if (c === connection && status === \"ready\") record(\"connection.blocked\", \"blocked\", String(reason));\n });\n c.on(\"unblocked\", () => {\n if (c !== connection || status !== \"blocked\") return;\n record(\"connection.unblocked\", \"ready\");\n readied.wake();\n });\n };\n\n const join = (c: RabbitConnection, ch: RabbitChannel, address: string): void => {\n connection = c;\n channel = ch;\n retries.reset();\n const ends = socketEnds(c.connection.stream);\n generation = joins.length + 1;\n const connectionName =\n ends.localAddress === null ? null : `${ends.localAddress}:${ends.localPort ?? \"?\"} -> ${ends.remoteAddress ?? \"?\"}:${ends.remotePort ?? \"?\"}`;\n joins.push({ generation, at: clock.now(), address, connectionName, ...ends });\n ch.on(\"error\", (error) => {\n if (ch === channel) record(\"channel.error\", status, message(error));\n });\n // amqplib closes each channel of a connection that ends, then it emits the close of the connection in the same turn.\n // Thus the client waits for that turn, and an end of the connection is not recorded as an end of the channel\n ch.on(\"close\", () => {\n queueMicrotask(() => {\n if (ch === channel) lose(\"channel.end\", \"the channel closed\");\n });\n });\n record(\"join\", \"ready\", `generation ${generation}, ${address}`);\n readied.wake();\n };\n\n /**\n * This function opens one generation: a connection, then its confirm channel, in `connect_bound`.\n * A failure goes to `failing`, and the backoff starts. A connection that arrives after its bound\n * or after the close is closed at once. The promise resolves when this try ends.\n */\n const open = (): Promise<void> => {\n const mine = (token += 1);\n const index = next;\n next += 1;\n const node = nodeOf(spec, index);\n const address = `${node.host}:${node.port}`;\n let opened: RabbitConnection | null = null;\n let finish: () => void = () => {};\n const ended = new Promise<void>((resolve) => (finish = resolve));\n const stale = (): boolean => mine !== token || closing();\n const fail = (detail: string): void => {\n bound();\n finish();\n if (stale()) return;\n token += 1;\n if (opened !== null) retire(opened);\n record(\"connect.failed\", \"failing\", `${address}: ${detail}`);\n scheduleRetry();\n };\n const bound = clock.after(ms(knobs.connect_bound), () => fail(\"no open connection and channel in connect_bound\"));\n void (async () => {\n const c = await create(rabbitOptions(name, spec, index));\n opened = c;\n watch(c);\n if (stale()) {\n retire(c);\n return;\n }\n const ch = await c.createConfirmChannel();\n if (stale()) {\n retire(c);\n return;\n }\n bound();\n join(c, ch, address);\n finish();\n })().catch((error: unknown) => fail(message(error)));\n return ended;\n };\n\n /** This function gives the channel for a call, or the reason that the call does not leave the client. */\n const usable = (signal: AbortSignal | undefined): RabbitChannel | Attempt<never> => {\n if (status === \"none\") return notSent(`${name} has not connected`);\n if (status !== \"ready\" || channel === null) return notSent(`${name} is ${status}, not ready: the call did not leave the client`);\n if (signal?.aborted) return notSent(\"the caller aborted the call before it left the client\");\n return channel;\n };\n\n /** This function makes one try of a publish: the write, then the confirm, in `request_timeout`. */\n const publishAttempt = (sent: RabbitPublish, signal: AbortSignal | undefined): Promise<Attempt<void>> => {\n const ch = usable(signal);\n if (\"outcome\" in ch) return Promise.resolve(ch);\n const content = typeof sent.content === \"string\" ? Buffer.from(sent.content, \"utf8\") : sent.content;\n return trackWithTimer<void>(\n flight,\n clock,\n requestTimeout,\n signal,\n \"the caller abandoned the publish after it was written\",\n \"no confirm in request_timeout: the broker can have the message\",\n (settle) => {\n try {\n ch.publish(sent.exchange, sent.routingKey, content, sent.properties ?? {}, (error) =>\n settle(error === null ? { outcome: \"ok\", value: undefined } : failureOutcome(error)),\n );\n } catch (error) {\n settle(failureOutcome(error));\n }\n },\n );\n };\n\n /**\n * This function makes one try of an RPC on the channel, in `request_timeout`. A reply that comes\n * after the call settled goes to `late`, thus a message that nobody receives goes back to its queue.\n */\n const rpcAttempt = <T, R>(\n signal: AbortSignal | undefined,\n what: string,\n run: (ch: RabbitChannel) => Promise<R>,\n value: (reply: R) => T,\n late?: (reply: R, ch: RabbitChannel) => void,\n ): Promise<Attempt<T>> => {\n const ch = usable(signal);\n if (\"outcome\" in ch) return Promise.resolve(ch);\n let finished = false;\n return trackWithTimer<T>(\n flight,\n clock,\n requestTimeout,\n signal,\n `the caller abandoned ${what} after it was sent`,\n `no reply to ${what} in request_timeout`,\n (settle) => {\n Promise.resolve()\n .then(() => run(ch))\n .then(\n (reply) => {\n if (finished) late?.(reply, ch);\n else settle({ outcome: \"ok\", value: value(reply) });\n },\n (error: unknown) => settle(failureOutcome(error)),\n );\n },\n () => {\n finished = true;\n },\n );\n };\n\n const publish = (operation: string, sent: RabbitPublish, sendOptions: SendOptions = {}): Promise<Sent<void>> =>\n sendWithReissue({\n clock,\n bound: requestTimeout,\n operation: operations[operation],\n options: sendOptions,\n attempt: (signal) => publishAttempt(sent, signal),\n // the client connects again by itself: the second try waits for a ready client, in the time that is left\n beforeAgain: (left, signal) => readied.wait(left, signal),\n onReissue: () => record(\"command.reissue\", status, operation),\n });\n\n const get = (operation: string, queue: string, sendOptions: SendOptions = {}): Promise<Sent<RabbitDelivery | null>> => {\n const at = (): number => generation;\n return sendWithReissue({\n clock,\n bound: requestTimeout,\n operation: operations[operation],\n options: sendOptions,\n attempt: (signal) => {\n const held = at();\n return rpcAttempt<RabbitDelivery | null, RabbitMessage | false>(\n signal,\n \"basic.get\",\n (ch) => ch.get(queue, { noAck: false }),\n (reply) => (reply === false ? null : { message: reply, generation: held, delivery: deliveryCount(reply) }),\n // a message after the timeout or the abort: nobody holds it, thus it goes back to its queue at once\n (reply, ch) => {\n if (reply === false) return;\n try {\n ch.nack(reply, false, true);\n } catch {\n // the channel ended, and the broker puts the message back by itself\n }\n },\n );\n },\n beforeAgain: (left, signal) => readied.wait(left, signal),\n onReissue: () => record(\"command.reissue\", status, operation),\n });\n };\n\n /** This function gives the channel that holds a delivery. A delivery of an ended generation is back on its queue already (at least once). */\n const holder = (job: RabbitDelivery): RabbitChannel => {\n const ch = channel;\n if (ch === null || job.generation !== generation)\n throw new Error(`${name}: the channel of generation ${job.generation} ended, thus the broker put the message back on its queue`);\n return ch;\n };\n\n const jobs = (operation: string, queue: string): RabbitJobs => ({\n source: async (signal) => {\n const taken = await get(operation, queue, { signal });\n if (taken.outcome === \"ok\") return taken.value;\n throw new Error(`${name} could not take a message from ${queue}: ${taken.error}`);\n },\n settle: {\n ack: (job) => holder(job).ack(job.message),\n requeue: (job) => holder(job).nack(job.message, false, true),\n reject: (job) => holder(job).nack(job.message, false, false),\n delivery: (job) => job.delivery,\n },\n });\n\n const handle: DependencyHandle = {\n async connect(signal) {\n if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);\n record(\"status.connecting\", \"connecting\");\n // a broker that is down does not stop the startup: the client connects again in the background,\n // and its checks keep readiness tripped until it is ready (readiness is earned)\n await open();\n },\n async close() {\n if (status === \"none\" || closing()) return;\n record(\"close.request\", \"closing\");\n retries.cancel();\n // an open in flight is stale now: its connection is closed when it arrives\n token += 1;\n const c = connection;\n if (c !== null) {\n // behind a black hole, connection.close waits for close-ok with no bound: the bound destroys the socket\n if (!(await withinBound(clock, ms(knobs.close_bound), Promise.resolve().then(() => c.close())))) {\n record(\"close.bound\", \"closing\");\n record(\"close.destroy\", \"closing\");\n c.connection.stream?.destroy?.();\n }\n }\n connection = null;\n channel = null;\n flight.settleAll(\"the client closed with the call in flight\");\n record(\"status.end\", \"end\");\n },\n };\n\n const heartbeat = finiteMs(knobs.heartbeat_interval);\n const declaration: Omit<Dependency, \"name\"> = {\n system: spec.system,\n knobs: {\n connect_bound: knobs.connect_bound,\n request_timeout: knobs.request_timeout,\n heartbeat_interval: knobs.heartbeat_interval,\n // each side checks for data at each heartbeat, and ends the connection after two checks with\n // none: 2 to 3 heartbeats after the last data. The cells measured 45.009 s at 15 s\n heartbeat_detection_time: heartbeat === undefined ? knobs.heartbeat_interval : quantity((3 * heartbeat) / 1000, \"s\"),\n retry_base: knobs.retry_base,\n retry_cap: knobs.retry_cap,\n keepalive_idle: knobs.keepalive_idle,\n // each side sends a heartbeat frame each half heartbeat when it sent nothing: that refreshes an idle flow (O03)\n idle_refresh_interval: heartbeat === undefined ? knobs.keepalive_idle : quantity(heartbeat / 2000, \"s\"),\n max_connection_age: knobs.max_connection_age,\n close_bound: knobs.close_bound,\n retrans_giveup_time: knobs.retrans_giveup_time,\n },\n operations,\n };\n\n const checks = (checkOptions: CheckOptions) =>\n stepChecks<ClientStep>(name, checkOptions, {\n // the state of the client, without a round trip\n \"local-state\": localStateCheck(() => status === \"ready\"),\n // a passive declare of an exchange of each broker, on the channel of the application\n \"round-trip\": roundTripCheck((sendOptions) =>\n sendWithReissue({\n clock,\n bound: requestTimeout,\n operation: undefined,\n options: sendOptions,\n attempt: (signal) => rpcAttempt(signal, \"a passive exchange.declare\", (ch) => ch.checkExchange(CHECK_EXCHANGE), () => undefined),\n onReissue: () => {},\n }),\n ),\n // a new connection each time, with the protocol header and no login\n dial: dialCheck(dial, spec.host, spec.port),\n });\n\n return {\n name,\n declaration,\n handle,\n meets: rabbitMeetings(name, knobs),\n get status() {\n return status;\n },\n joins,\n publish,\n get,\n jobs,\n checks,\n attach: (svc, checkOptions) => attachClient(svc, name, declaration, handle, checks(checkOptions)),\n problems: (description) => clientProblems(name, description, clientBounds(knobs)),\n };\n};\n","import type { Machine } from \"@mark1russell7/algebra\";\nimport { clientKnob as knob, closeTimer, transition as t } from \"../client/machine.ts\";\n\n/** The states where a generation is open, and the states where the client can close. */\nconst OPEN = [\"ready\", \"blocked\"] as const;\nconst SERVING = [\"connecting\", ...OPEN, \"failing\"] as const;\nconst LIVE = [...SERVING, \"closing\"] as const;\n\n/**\n * The RabbitMQ client of the service as a `mark1russell7.machine/1` document. It is one generation\n * at a time: one connection of amqplib and its confirm channel. The machines `amqplib` and\n * `rabbitmq-connection` of `@mark1russell7/machines` give the connection and its heartbeat. The states:\n * - `ready`: the channel is open, and the client sends each call.\n * - `blocked`: a resource alarm of the broker blocked the connection. Each call is not sent until the unblock.\n * - `failing`: no generation is open. The client tries the next node after its backoff.\n *\n * The end of the channel or of the connection ends the generation. A test records the transitions\n * of the client and replays them against this document.\n */\nexport const RABBIT_CLIENT_MACHINE: Machine = {\n format: \"mark1russell7.machine/1\",\n id: \"service.machine.rabbitmq-client\",\n title: \"The RabbitMQ client of the service, over amqplib 2\",\n layer: \"driver\",\n version: \"0.1.0\",\n sources: [{ ref: \"packages/service/src/rabbitmq (the client implements this document)\", rung: \"implemented\" }],\n states: {\n none: { initial: true, note: \"the client exists, and its first connection does not\" },\n connecting: { note: \"a connection, its handshake, and the open of its confirm channel, in connect_bound\" },\n ready: { note: \"the channel is open. A black hole stays hidden until the heartbeat or a timer of a call ends it\" },\n blocked: { note: \"a resource alarm blocked the connection: the broker does not read its socket\" },\n failing: { note: \"no generation is open. The client tries the next node after its backoff\" },\n closing: { note: \"connection.close, in close_bound, then a destroy of the socket\" },\n end: { final: true, note: \"the service closed the client\" },\n },\n events: {\n \"status.connecting\": { kind: \"local\" },\n join: { kind: \"rx\", note: \"the confirm channel is open: a new generation (D1)\" },\n \"connect.failed\": { kind: \"local\", note: \"the connection or its channel failed, or did not open in connect_bound\" },\n \"retry.connect\": { kind: \"timer\", note: \"the backoff ended: a try on the next node\" },\n \"connection.error\": { kind: \"rx\", note: \"an error event of the connection. Its close follows\" },\n \"connection.end\": { kind: \"rx\", note: \"the connection closed: a reset, a FIN, the heartbeat, or a close of the broker\" },\n \"channel.error\": { kind: \"rx\", note: \"the broker closed the channel with a reply code\" },\n \"channel.end\": { kind: \"rx\", note: \"the channel closed on a connection that stays: the client closes that connection too\" },\n \"connection.blocked\": { kind: \"rx\", note: \"connection.blocked of the broker: a resource alarm\" },\n \"connection.unblocked\": { kind: \"rx\" },\n \"command.reissue\": { kind: \"local\" },\n \"close.request\": { kind: \"local\" },\n \"close.bound\": { kind: \"timer\", note: \"connection.close got no close-ok in close_bound\" },\n \"close.destroy\": { kind: \"local\", note: \"the bound destroys a socket that a silent broker does not close\" },\n \"status.end\": { kind: \"local\" },\n },\n knobs: {\n connect_bound: knob(\"socketOptions.timeout\"),\n heartbeat_interval: knob(\"heartbeat\"),\n retry_base: knob(\"retry.base\"),\n retry_cap: knob(\"retry.cap\"),\n close_bound: knob(\"close.bound\"),\n },\n timers: {\n connect_timeout: {\n duration: { knob: \"connect_bound\", unit: \"s\" },\n arms: [\"enter:connecting\"],\n cancels: [\"join\", \"connect.failed\"],\n fires: \"connect.failed\",\n note: \"the bound of the wrapper on the connection and its channel. amqplib bounds the connect and the handshake with the same time\",\n },\n heartbeat: {\n duration: { expr: \"3 * heartbeat_interval\", unit: \"s\" },\n arms: [\"join\"],\n cancels: [\"connection.end\", \"close.request\"],\n fires: \"connection.end\",\n note: \"the heartbeat of amqplib ends a silent connection 2 to 3 heartbeats after the last data. Each frame that arrives starts it again\",\n },\n retry: {\n duration: { expr: \"min(retry_cap, retry_base * pow(2, fired)) + retry_base\", unit: \"s\" },\n arms: [\"connect.failed\", \"connection.end\", \"channel.end\"],\n cancels: [\"close.request\"],\n fires: \"retry.connect\",\n backoff: { resets: [\"join\"] },\n note: \"the most delay: the backoff doubles from retry_base to retry_cap, with a jitter up to retry_base\",\n },\n close: closeTimer(),\n },\n transitions: [\n t(\"connect\", \"none\", \"status.connecting\", \"connecting\"),\n t(\"join\", \"connecting\", \"join\", \"ready\"),\n t(\"connect-failed\", \"connecting\", \"connect.failed\", \"failing\", { timer: \"connect_timeout\" }),\n t(\"retry\", \"failing\", \"retry.connect\", \"connecting\", { timer: \"retry\" }),\n t(\"connection-end\", [...OPEN], \"connection.end\", \"failing\", { timer: \"heartbeat\" }),\n t(\"channel-end\", [...OPEN], \"channel.end\", \"failing\"),\n t(\"blocked\", \"ready\", \"connection.blocked\", \"blocked\"),\n t(\"unblocked\", \"blocked\", \"connection.unblocked\", \"ready\"),\n ...OPEN.map((state) => t(`connection-error-${state}`, state, \"connection.error\", state)),\n ...OPEN.map((state) => t(`channel-error-${state}`, state, \"channel.error\", state)),\n ...LIVE.map((state) => t(`reissue-${state}`, state, \"command.reissue\", state)),\n t(\"close\", [...SERVING], \"close.request\", \"closing\"),\n t(\"close-bound\", \"closing\", \"close.bound\", \"closing\", { timer: \"close\" }),\n t(\"close-destroy\", \"closing\", \"close.destroy\", \"closing\"),\n t(\"end\", \"closing\", \"status.end\", \"end\"),\n ],\n};\n","import { STARTING_POLL } from \"../client/core.ts\";\nimport type { Poll } from \"../timing.ts\";\nimport type { RabbitKnobs } from \"./client.ts\";\n\n/**\n * This poll is the poll of the checks that `RABBIT_STARTING_KNOBS` fits. It is `STARTING_POLL` of\n * the core: each 5 s with a jitter of 20 %, and a timeout of 4 s (B6, O12). The poll does not depend on the driver.\n */\nexport const RABBIT_STARTING_POLL: Poll = STARTING_POLL;\n\n/**\n * These knobs are a declaration to start from, from what the cells of R3 measured on amqplib 2.2.0\n * against RabbitMQ 4.3.6. The runtime has no defaults: a service gives these knobs, or its own.\n * Each value holds the borders that the cells measured:\n * - `connect_bound` 5 s: the connect timeout of DP44. The timer stops when the connection opens.\n * Thus it does not change a fault with a call in flight. At its defaults, amqplib has no connect timeout.\n * - `request_timeout` 9.5 s: in the slowest poll of 10 s (O13). After a loss of the leader of a quorum\n * queue, the first confirm came at 6.61 s, 6.68 s and 8.05 s (T11). A later confirm is indeterminate, not lost.\n * - `heartbeat_interval` 15 s: the heartbeat ended a silent connection 45.009 s after the last data,\n * before the kernel gave up at 53.016 s (O17). The frames each 7.5 s keep a flow through a router\n * that forgets it at 30 s (O03). At the 60 s of the broker, the heartbeat came at 180.009 s.\n * - `retry_base` 1 s: the cells connected to the next node 1 s after the end of a connection.\n * - `retry_cap` 8 s: no cell measured it. With its jitter of 1 s, the most delay is 9 s, in the slowest poll of 10 s.\n * - `keepalive_idle` \"infinite\": amqplib sets no keepalive, and the heartbeat refreshes an idle flow.\n * Linux sends no keepalive probe while data is in flight.\n * - `max_connection_age` \"infinite\": the client does not replace its connection at an age.\n * - `close_bound` 2 s: behind a black hole, `connection.close` waits for its close-ok with no bound.\n * - `retrans_giveup_time` 924.6 s: the give-up of Linux at `tcp_retries2 = 15`.\n */\nexport const RABBIT_STARTING_KNOBS: RabbitKnobs = {\n connect_bound: { value: 5, unit: \"s\" },\n request_timeout: { value: 9.5, unit: \"s\" },\n heartbeat_interval: { value: 15, unit: \"s\" },\n retry_base: { value: 1, unit: \"s\" },\n retry_cap: { value: 8, unit: \"s\" },\n keepalive_idle: { value: \"infinite\", unit: \"s\" },\n max_connection_age: { value: \"infinite\", unit: \"s\" },\n close_bound: { value: 2, unit: \"s\" },\n retrans_giveup_time: { value: 924.6, unit: \"s\" },\n};\n"],"mappings":"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"}
package/build/redis.d.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { t as Backoff } from "./backoff-B-jGWVDm.js";
2
1
  import { n as Clock } from "./clock-mVqhXD0b.js";
3
2
  import { tt as heartbeatDetection, wt as Operation } from "./lifecycle-fgYfxrHK.js";
3
+ import { C as MANIFESTATIONS, D as firstDetector, E as Situation, O as unwatched, S as Detector, T as Meeting, _ as Sent, a as ClientKnobs, c as ClientSpec, d as Factory, g as SendOptions, i as ClientJoin, l as ClientStep, n as CheckOptions, o as ClientOptions, p as RetryKnobs, r as Client, s as ClientSocket, t as Attempt, u as DialFn, w as Manifestation, y as retryBackoff } from "./core-BPLRDcUJ.js";
4
4
  import { n as LimiterStore } from "./store-D5HLgBnW.js";
5
- import { _ as Manifestation, a as ClientKnobs, b as firstDetector, c as ClientSpec, d as Factory, g as MANIFESTATIONS, h as Detector, i as ClientJoin, l as ClientStep, m as Sent, n as CheckOptions, o as ClientOptions, p as SendOptions, r as Client, s as ClientSocket, t as Attempt, u as DialFn, v as Meeting, x as unwatched, y as Situation } from "./core-C2wnF4mg.js";
6
5
  import { Extent, Quantity } from "@mark1russell7/quantity";
7
6
  import { Machine } from "@mark1russell7/algebra";
8
7
  //#region src/redis/connection.d.ts
@@ -69,7 +68,7 @@ export declare const ioredis: ConnectionFactory;
69
68
  * has them. The runtime has no defaults, thus each knob is necessary, and a bound that nobody gives
70
69
  * is "infinite", not zero. The knobs that each client has come from the core.
71
70
  */
72
- interface RedisKnobs extends ClientKnobs {
71
+ interface RedisKnobs extends ClientKnobs, RetryKnobs {
73
72
  /** This time bounds a try to connect, until the TCP connection exists (`connectTimeout`). */
74
73
  readonly connect_bound: Quantity;
75
74
  /** This time bounds each command, the handshake commands too (`commandTimeout`). */
@@ -128,11 +127,6 @@ interface RedisClient extends Client<ClientStep, Join> {
128
127
  */
129
128
  send<T = unknown>(command: string, args?: ReadonlyArray<string | number>, options?: SendOptions): Promise<Sent<T>>;
130
129
  }
131
- /**
132
- * This function gives the backoff of the tries to connect: from `retry_base`, doubling to
133
- * `retry_cap`, each with a jitter up to the base. The client rounds each delay to a whole millisecond.
134
- */
135
- export declare const retryBackoff: (knobs: RedisKnobs) => Backoff;
136
130
  /** This function gives the problems of a spec: the knobs that the client cannot use. */
137
131
  export declare const specProblems: (name: string, spec: RedisClientSpec) => string[];
138
132
  /** This dial function opens a TCP connection, sends an inline PING, and waits for +PONG. */
@@ -219,5 +213,5 @@ export declare const REDIS_OPERATIONS: Readonly<Record<string, Operation>>;
219
213
  /** This function gives the name of a command as the table writes it: upper case, and CLIENT ID with its subcommand. */
220
214
  export declare const commandName: (command: string, args: readonly unknown[]) => string;
221
215
  //#endregion
222
- export { type Attempt, type CheckOptions, type Client, type ClientJoin, type ClientKnobs, type ClientOptions, type ClientSocket, type ClientSpec, type ClientStep, type ConnectionFactory, type Detector, type DialFn, type Factory, type Join, MANIFESTATIONS, type Manifestation, type Meeting, type RedisClient, type RedisClientOptions, type RedisClientSpec, type RedisConnection, type RedisConnectionOptions, type RedisKnobs, type RedisLimiterOptions, type RedisSender, type RedisSocket, type Sent, type Situation, firstDetector, heartbeatDetection, unwatched };
216
+ export { type Attempt, type CheckOptions, type Client, type ClientJoin, type ClientKnobs, type ClientOptions, type ClientSocket, type ClientSpec, type ClientStep, type ConnectionFactory, type Detector, type DialFn, type Factory, type Join, MANIFESTATIONS, type Manifestation, type Meeting, type RedisClient, type RedisClientOptions, type RedisClientSpec, type RedisConnection, type RedisConnectionOptions, type RedisKnobs, type RedisLimiterOptions, type RedisSender, type RedisSocket, type Sent, type Situation, firstDetector, heartbeatDetection, retryBackoff, unwatched };
223
217
  //# sourceMappingURL=redis.d.ts.map
package/build/redis.js CHANGED
@@ -2,7 +2,7 @@ import { realClock } from "./clock.js";
2
2
  import { heartbeat } from "./heartbeat.js";
3
3
  import { i as heartbeatDetection } from "./timing-DGRi3XCI.js";
4
4
  import { backoffDelay } from "./backoff.js";
5
- import { A as keepaliveSilent, C as withinBound, D as immediate, E as firstDetector, M as unwatched, O as keepaliveAnswered, S as timeProblems, T as WHAT, _ as operationTable, b as socketEnds, c as clientProblems, d as finiteMs, f as inFlight, g as notSent, h as ms, i as helloDial, j as kernelGiveUp, k as keepaliveReset, l as clientRecorder, m as message, n as closeTimer, o as attachClient, p as localStateCheck, r as transition, s as clientBounds, t as clientKnob, u as dialCheck, v as roundTripCheck, w as MANIFESTATIONS, x as stepChecks, y as sendWithReissue } from "./machine-Czfb6fV4.js";
5
+ import { C as notSent, D as readyWait, F as timeProblems, G as immediate, J as keepaliveSilent, K as keepaliveAnswered, M as sendWithReissue, N as socketEnds, O as retryBackoff, P as stepChecks, R as withinBound, S as ms, U as WHAT, V as MANIFESTATIONS, W as firstDetector, X as unwatched, Y as kernelGiveUp, _ as inFlight, a as helloDial, b as message, d as clientProblems, f as clientRecorder, h as finiteMs, j as roundTripCheck, k as retryProblems, l as attachClient, m as dialCheck, n as closeTimer, q as keepaliveReset, r as transition, t as clientKnob, u as clientBounds, w as operationTable, x as missesProblems, y as localStateCheck } from "./machine-BhTRu3u9.js";
6
6
  //#region src/redis/connection.ts
7
7
  /**
8
8
  * This factory makes an ioredis client. It loads ioredis only when a client connects, thus a
@@ -197,29 +197,16 @@ const commandName = (command, args) => {
197
197
  };
198
198
  //#endregion
199
199
  //#region src/redis/client.ts
200
- /**
201
- * This function gives the backoff of the tries to connect: from `retry_base`, doubling to
202
- * `retry_cap`, each with a jitter up to the base. The client rounds each delay to a whole millisecond.
203
- */
204
- const retryBackoff = (knobs) => ({
205
- base: ms(knobs.retry_base),
206
- factor: 2,
207
- cap: ms(knobs.retry_cap),
208
- jitter: {
209
- kind: "add",
210
- amount: ms(knobs.retry_base)
211
- }
212
- });
213
200
  /** This function gives the problems of a spec: the knobs that the client cannot use. */
214
201
  const specProblems = (name, spec) => {
215
202
  const problems = [];
216
203
  const { knobs } = spec;
217
- if (!Number.isInteger(spec.heartbeatMisses) || spec.heartbeatMisses < 1) problems.push(`${name}'s watchdog closes the connection after ${spec.heartbeatMisses} misses, not a whole number above zero`);
204
+ problems.push(...missesProblems(name, spec.heartbeatMisses, "watchdog"));
218
205
  if (knobs.max_connection_age.value !== "infinite") problems.push(`${name} cannot replace its connection at an age: max_connection_age must be "infinite"`);
219
206
  problems.push(...timeProblems(name, knobs));
220
207
  if (ms(knobs.request_timeout) <= 0) problems.push(`${name}'s request_timeout is not above zero: each command must have a bound`);
221
208
  if (ms(knobs.heartbeat_timeout) <= 0) problems.push(`${name}'s heartbeat_timeout is not above zero`);
222
- if (ms(knobs.retry_cap) < ms(knobs.retry_base)) problems.push(`${name}'s retry_cap is below its retry_base`);
209
+ problems.push(...retryProblems(name, knobs));
223
210
  return problems;
224
211
  };
225
212
  /** This dial function opens a TCP connection, sends an inline PING, and waits for +PONG. */
@@ -259,7 +246,7 @@ const redisClient = (name, spec, options = {}) => {
259
246
  let closing = false;
260
247
  const joins = [];
261
248
  const flight = inFlight();
262
- const readyWaiters = /* @__PURE__ */ new Set();
249
+ const readied = readyWait(clock, () => connection?.status === "ready");
263
250
  const listeners = [];
264
251
  const record = clientRecorder(options.record, clock, name);
265
252
  let retrySince = null;
@@ -327,7 +314,7 @@ const redisClient = (name, spec, options = {}) => {
327
314
  ...socketEnds(socket)
328
315
  });
329
316
  record("ready", "ready", "join", `generation ${generation}, client id ${clientId ?? "unknown"}`);
330
- for (const wake of [...readyWaiters]) wake();
317
+ readied.wake();
331
318
  };
332
319
  const onStatus = (to) => () => {
333
320
  const from = status;
@@ -353,25 +340,6 @@ const redisClient = (name, spec, options = {}) => {
353
340
  c.on("error", onError);
354
341
  listeners.push(["error", onError]);
355
342
  };
356
- /** This function waits until the connection is ready, for `within` milliseconds at most. It gives true when it is ready. */
357
- const waitReady = (within, signal) => new Promise((resolve) => {
358
- if (connection?.status === "ready") {
359
- resolve(true);
360
- return;
361
- }
362
- let cancel = () => {};
363
- const done = (value) => {
364
- readyWaiters.delete(wake);
365
- cancel();
366
- signal?.removeEventListener("abort", aborted);
367
- resolve(value);
368
- };
369
- const wake = () => done(true);
370
- const aborted = () => done(false);
371
- readyWaiters.add(wake);
372
- cancel = clock.after(within, () => done(connection?.status === "ready"));
373
- signal?.addEventListener("abort", aborted, { once: true });
374
- });
375
343
  const attempt = (command, args, signal) => {
376
344
  const c = connection;
377
345
  if (c === null) return Promise.resolve(notSent(`${name} has not connected`));
@@ -390,7 +358,7 @@ const redisClient = (name, spec, options = {}) => {
390
358
  operation: operations[commandName(command, args)],
391
359
  options: sendOptions,
392
360
  attempt: (signal) => attempt(command, args, signal),
393
- beforeAgain: (left, signal) => waitReady(left, signal),
361
+ beforeAgain: (left, signal) => readied.wait(left, signal),
394
362
  onReissue: () => record(status, status, "command.reissue", commandName(command, args))
395
363
  });
396
364
  let watchdog = null;
@@ -1 +1 @@
1
- {"version":3,"file":"redis.js","names":["knob","t"],"sources":["../src/redis/connection.ts","../src/redis/meets.ts","../src/redis/operations.ts","../src/redis/client.ts","../src/redis/limiter.ts","../src/redis/machine.ts"],"sourcesContent":["import type { ClientSocket, Factory } from \"../client/core.ts\";\n\n/**\n * A Redis connection is the part of an ioredis client that the wrapper uses. An ioredis 6 client has\n * each part. A fake for tests has them too. Thus the package depends on no type of ioredis.\n */\nexport interface RedisConnection {\n /** This text is the state of the client: wait, connecting, connect, ready, close, reconnecting or end. */\n readonly status: string;\n /** This socket is the socket of the current connection, when there is one. */\n readonly stream?: RedisSocket | undefined;\n connect(): Promise<void>;\n /** This method sends one command, and gives the reply. */\n call(command: string, ...args: Array<string | number>): Promise<unknown>;\n quit(): Promise<unknown>;\n /** This method closes the connection at once. With `true`, the client then connects again. */\n disconnect(reconnect?: boolean): void;\n on(event: string, listener: (...args: unknown[]) => void): unknown;\n off(event: string, listener: (...args: unknown[]) => void): unknown;\n}\n\n/** A Redis socket is the socket of a client in the core: the join of the prober and the bounded close read it. */\nexport type RedisSocket = ClientSocket;\n\n/**\n * These are the options that the wrapper gives ioredis. Each one comes from the declaration. The\n * wrapper sets each option that has an effect on a timer or on a send again, thus no default of\n * ioredis decides one.\n */\nexport interface RedisConnectionOptions {\n readonly host: string;\n readonly port: number;\n readonly db?: number | undefined;\n readonly username?: string | undefined;\n readonly password?: string | undefined;\n /** The wrapper connects in its own step of the startup. */\n readonly lazyConnect: true;\n readonly connectTimeout: number;\n readonly commandTimeout: number;\n /** This time destroys a socket that gives no data while a command waits. Undefined is no bound. */\n readonly socketTimeout: number | undefined;\n /** This time is the idle time of TCP keepalive. False is no keepalive. */\n readonly keepAlive: number | false;\n /** A command while the client is not ready fails at once: it did not reach the server. */\n readonly enableOfflineQueue: false;\n /** The client does not send again a command that can have reached the server. */\n readonly autoResendUnfulfilledCommands: false;\n readonly maxRetriesPerRequest: null;\n readonly enableReadyCheck: true;\n readonly connectionName: string;\n /** This function gives the delay before the next try to connect, or null to stop. */\n readonly retryStrategy: (times: number) => number | null;\n}\n\n/** A connection factory makes the connection of the client from its options. */\nexport type ConnectionFactory = Factory<RedisConnectionOptions, RedisConnection>;\n\n/**\n * This factory makes an ioredis client. It loads ioredis only when a client connects, thus a\n * service that does not use Redis does not need ioredis.\n */\nexport const ioredis: ConnectionFactory = async (options) => {\n const loaded = (await import(\"ioredis\")) as unknown as {\n readonly default: new (options: RedisConnectionOptions) => RedisConnection;\n };\n return new loaded.default(options);\n};\n","import type { Extent } from \"@mark1russell7/quantity\";\nimport type { Detector, Meeting, Situation } from \"../client/meets.ts\";\nimport { WHAT, immediate, keepaliveAnswered, keepaliveReset, keepaliveSilent, kernelGiveUp } from \"../client/meets.ts\";\nimport type { RedisKnobs } from \"./client.ts\";\n\n// the shapes and the queries moved to the core of the clients: this entry keeps them for the users of 0.5\nexport type { Detector, Meeting, Situation } from \"../client/meets.ts\";\nexport { firstDetector, unwatched } from \"../client/meets.ts\";\n\n/**\n * This function gives the manifestations that a Redis client can meet, and the detectors of each, from\n * its knobs. A reset or a FIN arrives on the next segment. A black hole and a host that is gone give\n * no answer at all. A frozen process and a stalled server keep a kernel that acknowledges each\n * segment. Thus TCP keepalive does not fire on them, and only the client's own timers find them.\n */\nexport const meetings = (name: string, knobs: RedisKnobs, heartbeat: Extent): Meeting[] => {\n const watchdog = (situation: Situation): Detector => ({\n by: \"heartbeat_detection_time\",\n within: heartbeat,\n situation,\n effect: `the watchdog of ${name} closes the connection, and the client connects again`,\n });\n const writeoff: Detector = {\n by: \"driver_writeoff_time\",\n within: knobs.driver_writeoff_time,\n situation: \"in flight\",\n effect: \"ioredis destroys the socket of a command without data, and the client connects again\",\n };\n const command: Detector = {\n by: \"request_timeout\",\n within: knobs.request_timeout,\n situation: \"in flight\",\n effect: \"the command fails as indeterminate, and the state of the connection does not change\",\n };\n const giveUp = kernelGiveUp(knobs.retrans_giveup_time);\n const silent = (manifestation: \"blackhole\" | \"freeze-host\"): Meeting => ({\n manifestation,\n what: WHAT[manifestation],\n detectors: [command, writeoff, watchdog(\"in flight\"), giveUp, watchdog(\"idle\"), keepaliveSilent(knobs.keepalive_idle)],\n });\n const acknowledged = (manifestation: string, what: string): Meeting => ({\n manifestation,\n what,\n detectors: [command, writeoff, watchdog(\"in flight\"), watchdog(\"idle\"), keepaliveAnswered()],\n });\n return [\n {\n manifestation: \"rst\",\n what: WHAT.rst,\n detectors: [\n immediate(\"in flight\", \"the command fails, and the client connects again, and each attempt is refused\"),\n watchdog(\"idle\"),\n keepaliveReset(knobs.keepalive_idle),\n ],\n },\n {\n manifestation: \"fin\",\n what: WHAT.fin,\n detectors: [immediate(\"in flight\", \"the close arrives, and the command fails\"), immediate(\"idle\", \"the close arrives\")],\n },\n silent(\"blackhole\"),\n silent(\"freeze-host\"),\n acknowledged(\"freeze-process\", WHAT[\"freeze-process\"]),\n acknowledged(\"app-stall\", \"the server stops its replies for a time, for example a long command, and its kernel stays up\"),\n ];\n};\n","import type { Operation } from \"../operation.ts\";\n\n/**\n * These are the Redis commands whose idempotency the client declares when the declaration does not\n * say. A second issue of an idempotent command has the same effect. Thus the client can send it\n * again after a failure that left its result unknown. The client does not send again a command\n * that is not in this table: a declaration that uses it says its idempotency itself.\n */\nconst SAME_EFFECT = [\n // reads\n \"PING\",\n \"ECHO\",\n \"GET\",\n \"MGET\",\n \"STRLEN\",\n \"GETRANGE\",\n \"EXISTS\",\n \"TYPE\",\n \"TTL\",\n \"PTTL\",\n \"EXPIRETIME\",\n \"HGET\",\n \"HMGET\",\n \"HGETALL\",\n \"HKEYS\",\n \"HVALS\",\n \"HLEN\",\n \"HEXISTS\",\n \"SMEMBERS\",\n \"SISMEMBER\",\n \"SMISMEMBER\",\n \"SCARD\",\n \"ZRANGE\",\n \"ZRANGEBYSCORE\",\n \"ZSCORE\",\n \"ZMSCORE\",\n \"ZRANK\",\n \"ZCARD\",\n \"ZCOUNT\",\n \"LRANGE\",\n \"LINDEX\",\n \"LLEN\",\n \"XRANGE\",\n \"XREVRANGE\",\n \"XLEN\",\n \"SCAN\",\n \"SSCAN\",\n \"HSCAN\",\n \"ZSCAN\",\n \"DBSIZE\",\n \"INFO\",\n \"CLIENT ID\",\n // writes that put a state, not a change of a state: a second issue puts the same state\n \"SET\",\n \"MSET\",\n \"SETEX\",\n \"PSETEX\",\n \"DEL\",\n \"UNLINK\",\n \"EXPIRE\",\n \"PEXPIRE\",\n \"EXPIREAT\",\n \"PEXPIREAT\",\n \"PERSIST\",\n \"HSET\",\n \"HDEL\",\n \"SADD\",\n \"SREM\",\n \"ZADD\",\n \"ZREM\",\n];\n\n/**\n * These commands change a state relative to the state before them. A second issue changes it again:\n * an INCR that the client sends again after a timeout counts two times.\n */\nconst CHANGE = [\n \"INCR\",\n \"INCRBY\",\n \"INCRBYFLOAT\",\n \"DECR\",\n \"DECRBY\",\n \"HINCRBY\",\n \"HINCRBYFLOAT\",\n \"ZINCRBY\",\n \"APPEND\",\n \"LPUSH\",\n \"RPUSH\",\n \"LPOP\",\n \"RPOP\",\n \"BLPOP\",\n \"BRPOP\",\n \"LMOVE\",\n \"BLMOVE\",\n \"SPOP\",\n \"XADD\",\n \"XACK\",\n \"PUBLISH\",\n \"EVAL\",\n \"EVALSHA\",\n \"FCALL\",\n \"MULTI\",\n \"EXEC\",\n \"GETDEL\",\n \"GETSET\",\n \"SETNX\",\n];\n\n/** These are the operations of the client: the idempotency of each command that the table names. */\nexport const REDIS_OPERATIONS: Readonly<Record<string, Operation>> = Object.fromEntries([\n ...SAME_EFFECT.map((command) => [command, { idempotent: true }] as const),\n ...CHANGE.map((command) => [command, { idempotent: false }] as const),\n]);\n\n/** This function gives the name of a command as the table writes it: upper case, and CLIENT ID with its subcommand. */\nexport const commandName = (command: string, args: readonly unknown[]): string => {\n const name = command.toUpperCase();\n if ((name === \"CLIENT\" || name === \"CONFIG\" || name === \"DEBUG\") && typeof args[0] === \"string\")\n return `${name} ${args[0].toUpperCase()}`;\n return name;\n};\n","import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport type { Backoff } from \"../backoff.ts\";\nimport { backoffDelay } from \"../backoff.ts\";\nimport type { Cancel } from \"../clock.ts\";\nimport { realClock } from \"../clock.ts\";\nimport type { Attempt, CheckOptions, Client, ClientJoin, ClientKnobs, ClientOptions, ClientSpec, ClientStep, DialFn, SendOptions, Sent } from \"../client/core.ts\";\nimport {\n attachClient,\n clientBounds,\n clientProblems,\n clientRecorder,\n dialCheck,\n finiteMs,\n inFlight,\n localStateCheck,\n message,\n ms,\n notSent,\n operationTable,\n roundTripCheck,\n sendWithReissue,\n socketEnds,\n stepChecks,\n timeProblems,\n withinBound,\n} from \"../client/core.ts\";\nimport { helloDial } from \"../client/dial.ts\";\nimport type { Dependency } from \"../description.ts\";\nimport type { Heartbeat } from \"../heartbeat.ts\";\nimport { heartbeat } from \"../heartbeat.ts\";\nimport type { DependencyHandle } from \"../lifecycle.ts\";\nimport { heartbeatDetection } from \"../timing.ts\";\nimport type { ConnectionFactory, RedisConnection, RedisConnectionOptions } from \"./connection.ts\";\nimport { ioredis } from \"./connection.ts\";\nimport { meetings } from \"./meets.ts\";\nimport { REDIS_OPERATIONS, commandName } from \"./operations.ts\";\n\n// the three outcomes, the send options and the checks moved to the core of the clients: this entry keeps them for the users of 0.5\nexport type { CheckOptions, ClientStep, DialFn, SendOptions, Sent } from \"../client/core.ts\";\n\n/**\n * These are the knobs of the client, each a time, by the names of the ordering law where the law\n * has them. The runtime has no defaults, thus each knob is necessary, and a bound that nobody gives\n * is \"infinite\", not zero. The knobs that each client has come from the core.\n */\nexport interface RedisKnobs extends ClientKnobs {\n /** This time bounds a try to connect, until the TCP connection exists (`connectTimeout`). */\n readonly connect_bound: Quantity;\n /** This time bounds each command, the handshake commands too (`commandTimeout`). */\n readonly request_timeout: Quantity;\n /** This time destroys a socket that gives no data while a command waits (`socketTimeout`). */\n readonly driver_writeoff_time: Extent;\n /** This time is the interval of the in-band watchdog: a PING on the connection of the application. */\n readonly heartbeat_interval: Extent;\n /** This time bounds each PING of the watchdog. */\n readonly heartbeat_timeout: Quantity;\n /** This time is how long the client tries to connect again before it stops. The watchdog then starts it again. */\n readonly client_retry_window: Extent;\n /** This time is the first delay before a new try to connect. The delay doubles after each try. */\n readonly retry_base: Quantity;\n /** This time is the most that one delay can be, before its jitter. */\n readonly retry_cap: Quantity;\n /** This time bounds the close of the client at the shutdown: a QUIT, then a disconnect. */\n readonly close_bound: Quantity;\n /** This time is the age at which the client replaces its connection. The client supports only \"infinite\". */\n readonly max_connection_age: Extent;\n}\n\n/** A Redis client spec is the declaration of one Redis client: where it connects, and its knobs. */\nexport interface RedisClientSpec extends ClientSpec<RedisKnobs> {\n readonly db?: number | undefined;\n readonly username?: string | undefined;\n /** This count is the number of PINGs in sequence without an answer that make the watchdog connect again. */\n readonly heartbeatMisses: number;\n}\n\n/** These options make a client. Without them, it uses ioredis, the real clock and `Math.random`. */\nexport interface RedisClientOptions extends ClientOptions<ConnectionFactory> {\n readonly random?: (() => number) | undefined;\n /**\n * This function waits one turn of the event loop. The watchdog waits it before it counts a miss,\n * thus a reply that is in the socket is read first. Without it, the client uses `setImmediate`.\n */\n readonly turn?: (() => Promise<void>) | undefined;\n}\n\n/**\n * A join is one generation of the connection, at its `ready` (D1, D2). The prober joins its kernel\n * records to the server's view of the connection through it. A join holds the server's id of the\n * connection, and the local address and port of the socket.\n */\nexport interface Join extends ClientJoin {\n /** This number is the reply of CLIENT ID, or null when the server gave none. */\n readonly clientId: number | null;\n}\n\n/** A Redis client is a dependency of a service: its declaration, its handle, its checks and its commands. */\nexport interface RedisClient extends Client<ClientStep, Join> {\n /** This text is the state of the connection, as ioredis names it, or \"none\" before the connect. */\n readonly status: string;\n /**\n * This method sends a command, and gives its outcome in three values. The client sends again\n * only an idempotent operation, one time at most, after an outcome that is not certain. The\n * second try stays in the bound of the request. The client does not send again an operation that\n * the table does not name.\n */\n send<T = unknown>(command: string, args?: ReadonlyArray<string | number>, options?: SendOptions): Promise<Sent<T>>;\n}\n\n/**\n * This function gives the backoff of the tries to connect: from `retry_base`, doubling to\n * `retry_cap`, each with a jitter up to the base. The client rounds each delay to a whole millisecond.\n */\nexport const retryBackoff = (knobs: RedisKnobs): Backoff => ({\n base: ms(knobs.retry_base),\n factor: 2,\n cap: ms(knobs.retry_cap),\n jitter: { kind: \"add\", amount: ms(knobs.retry_base) },\n});\n\n/** This function gives the problems of a spec: the knobs that the client cannot use. */\nexport const specProblems = (name: string, spec: RedisClientSpec): string[] => {\n const problems: string[] = [];\n const { knobs } = spec;\n if (!Number.isInteger(spec.heartbeatMisses) || spec.heartbeatMisses < 1)\n problems.push(`${name}'s watchdog closes the connection after ${spec.heartbeatMisses} misses, not a whole number above zero`);\n if (knobs.max_connection_age.value !== \"infinite\")\n problems.push(`${name} cannot replace its connection at an age: max_connection_age must be \"infinite\"`);\n problems.push(...timeProblems(name, knobs));\n if (ms(knobs.request_timeout) <= 0) problems.push(`${name}'s request_timeout is not above zero: each command must have a bound`);\n if (ms(knobs.heartbeat_timeout) <= 0) problems.push(`${name}'s heartbeat_timeout is not above zero`);\n if (ms(knobs.retry_cap) < ms(knobs.retry_base)) problems.push(`${name}'s retry_cap is below its retry_base`);\n return problems;\n};\n\n/** This dial function opens a TCP connection, sends an inline PING, and waits for +PONG. */\nexport const tcpDial: DialFn = helloDial(\"PING\\r\\n\", (received) => {\n const reply = received.toString();\n if (!reply.includes(\"\\r\\n\")) return \"more\";\n return reply.startsWith(\"+PONG\") ? null : new Error(`the reply was ${reply.trim()}`);\n});\n\n/**\n * This function makes a Redis client. It is a dependency of a service, with ioredis under it:\n * - It sets each timer of ioredis from the declaration, and it does not let ioredis send a command\n * again (R5 §8).\n * - It tells the outcome of a command in three values, and it settles each command in flight as\n * indeterminate when its connection closes (C8, C10).\n * - It records the server's id of each connection at `ready` (D1).\n * - A watchdog sends a PING in the band of the connection, and it connects again after the misses\n * of the declaration. It also starts a client that stopped (principle 19).\n *\n * The outcomes, the second try, the commands in flight, the bounded close and the checks come\n * from the core of the clients. This entry gives what ioredis does in its own way.\n */\nexport const redisClient = (name: string, spec: RedisClientSpec, options: RedisClientOptions = {}): RedisClient => {\n const problems = specProblems(name, spec);\n if (problems.length > 0) throw new Error(problems.join(\"; \"));\n const { knobs } = spec;\n const clock = options.clock ?? realClock;\n const random = options.random ?? Math.random;\n const create = options.create ?? ioredis;\n const dial = options.dial ?? tcpDial;\n const turn = options.turn ?? (() => new Promise<void>((resolve) => setImmediate(resolve)));\n const operations = operationTable(REDIS_OPERATIONS, spec.operations);\n const requestTimeout = ms(knobs.request_timeout);\n const detection = heartbeatDetection(knobs.heartbeat_interval, knobs.heartbeat_timeout, spec.heartbeatMisses);\n const backoff = retryBackoff(knobs);\n\n let connection: RedisConnection | null = null;\n let status = \"none\";\n let closing = false;\n const joins: Join[] = [];\n // the commands that the client wrote while the connection was ready: a close settles each one\n const flight = inFlight();\n const readyWaiters = new Set<() => void>();\n const listeners: Array<[string, (...args: unknown[]) => void]> = [];\n\n const record = clientRecorder(options.record, clock, name);\n\n // the delays of the tries to connect (`retryBackoff`), and none after the retry window. The\n // watchdog starts the client again after that\n let retrySince: number | null = null;\n const retryStrategy = (times: number): number | null => {\n const now = clock.now();\n retrySince ??= now;\n if (now - retrySince > ms(knobs.client_retry_window)) {\n record(status, status, \"retry.window-ended\");\n return null;\n }\n return Math.round(backoffDelay(backoff, times, random));\n };\n\n const connectionOptions: RedisConnectionOptions = {\n host: spec.host,\n port: spec.port,\n ...(spec.db === undefined ? {} : { db: spec.db }),\n ...(spec.username === undefined ? {} : { username: spec.username }),\n ...(spec.password === undefined ? {} : { password: spec.password }),\n lazyConnect: true,\n connectTimeout: ms(knobs.connect_bound),\n commandTimeout: requestTimeout,\n // ioredis reads undefined as no socket timeout, and false as no keepalive\n socketTimeout: finiteMs(knobs.driver_writeoff_time),\n keepAlive: finiteMs(knobs.keepalive_idle) ?? false,\n enableOfflineQueue: false,\n autoResendUnfulfilledCommands: false,\n maxRetriesPerRequest: null,\n enableReadyCheck: true,\n connectionName: name,\n retryStrategy,\n };\n\n /** This function gives the outcome of a failed command: a reply of the server, a command that did not leave the client, or neither. */\n const failed = (error: unknown): Attempt => {\n const text = message(error);\n if (error instanceof Error && error.name === \"ReplyError\") return { outcome: \"error\", error: text, reply: true };\n if (/isn't writeable|enableOfflineQueue/i.test(text)) return notSent(text);\n return { outcome: \"indeterminate\", error: text };\n };\n\n const onReady = async (): Promise<void> => {\n retrySince = null;\n const c = connection;\n if (c === null) return;\n const generation = joins.length + 1;\n const at = clock.now();\n const socket = c.stream;\n let clientId: number | null = null;\n try {\n const reply = await c.call(\"CLIENT\", \"ID\");\n clientId = typeof reply === \"number\" ? reply : Number(reply);\n if (!Number.isFinite(clientId)) clientId = null;\n } catch {\n clientId = null;\n }\n // a connection that closed before its reply is a generation that never served\n if (connection !== c || c.status !== \"ready\") return;\n joins.push({ generation, at, clientId, ...socketEnds(socket) });\n record(\"ready\", \"ready\", \"join\", `generation ${generation}, client id ${clientId ?? \"unknown\"}`);\n for (const wake of [...readyWaiters]) wake();\n };\n\n const onStatus = (to: string) => (): void => {\n const from = status;\n status = to;\n record(from, to, `status.${to}`);\n // the lower layer gave up the connection: each command in flight learns it now (C10)\n if (to === \"close\" || to === \"end\") flight.settleAll(`the connection closed with the command in flight (${from})`);\n if (to === \"ready\") void onReady();\n };\n\n const watch = (c: RedisConnection): void => {\n for (const state of [\"connecting\", \"connect\", \"ready\", \"close\", \"reconnecting\", \"end\"]) {\n const listener = onStatus(state);\n c.on(state, listener);\n listeners.push([state, listener]);\n }\n // an error of the connection comes before its close, which settles the commands: here it is only recorded\n const onError = (error: unknown): void => record(status, status, \"error\", message(error));\n c.on(\"error\", onError);\n listeners.push([\"error\", onError]);\n };\n\n /** This function waits until the connection is ready, for `within` milliseconds at most. It gives true when it is ready. */\n const waitReady = (within: number, signal?: AbortSignal): Promise<boolean> =>\n new Promise((resolve) => {\n if (connection?.status === \"ready\") {\n resolve(true);\n return;\n }\n let cancel: Cancel = () => {};\n const done = (value: boolean): void => {\n readyWaiters.delete(wake);\n cancel();\n signal?.removeEventListener(\"abort\", aborted);\n resolve(value);\n };\n const wake = (): void => done(true);\n const aborted = (): void => done(false);\n readyWaiters.add(wake);\n cancel = clock.after(within, () => done(connection?.status === \"ready\"));\n signal?.addEventListener(\"abort\", aborted, { once: true });\n });\n\n const attempt = (command: string, args: ReadonlyArray<string | number>, signal?: AbortSignal): Promise<Attempt> => {\n const c = connection;\n if (c === null) return Promise.resolve(notSent(`${name} has not connected`));\n // ioredis has no offline queue here: a command while the client is not ready fails at once, before it leaves the client\n if (c.status !== \"ready\") return Promise.resolve(notSent(`${name} is ${c.status}, not ready: the command did not leave the client`));\n if (signal?.aborted) return Promise.resolve(notSent(\"the caller aborted the command before it left the client\"));\n return flight.track(signal, \"the caller abandoned the command after it was sent\", (settle) => {\n c.call(command, ...args).then(\n (value) => settle({ outcome: \"ok\", value }),\n (error: unknown) => settle(failed(error)),\n );\n });\n };\n\n const send = <T>(command: string, args: ReadonlyArray<string | number> = [], sendOptions: SendOptions = {}): Promise<Sent<T>> =>\n sendWithReissue({\n clock,\n bound: requestTimeout,\n operation: operations[commandName(command, args)],\n options: sendOptions,\n attempt: (signal) => attempt(command, args, signal) as Promise<Attempt<T>>,\n // ioredis connects again by itself: the second try waits for a ready connection, in the time that is left\n beforeAgain: (left, signal) => waitReady(left, signal),\n onReissue: () => record(status, status, \"command.reissue\", commandName(command, args)),\n });\n\n // the watchdog: a heartbeat of PINGs on the connection of the application, each interval from the\n // start of the one before. A reply, an error reply too, is an answer. It waits one turn of the\n // event loop before it counts a miss, thus a slow loop is not a silent server. It acts only on a\n // ready connection, thus it never closes a connection in the middle of a new attempt\n let watchdog: Heartbeat | null = null;\n const startWatchdog = (): Heartbeat =>\n heartbeat({\n interval: ms(knobs.heartbeat_interval),\n timeout: ms(knobs.heartbeat_timeout),\n misses: spec.heartbeatMisses,\n clock,\n turn,\n // a PING starts at each tick, also while an earlier PING waits\n overlap: true,\n due: () => {\n const c = connection;\n if (c === null || closing) return false;\n if (c.status === \"end\") {\n // the retry window ended: the watchdog starts the client again\n record(status, status, \"watchdog.revive\");\n retrySince = null;\n c.connect().catch(() => {});\n return false;\n }\n return c.status === \"ready\";\n },\n ping: (signal) =>\n attempt(\"PING\", [], signal).then((result) => result.outcome === \"ok\" || (result.outcome === \"error\" && result.reply)),\n live: () => connection?.status === \"ready\" && !closing,\n onMiss: (misses) => record(status, status, \"watchdog.miss\", `${misses} of ${spec.heartbeatMisses}`),\n onDead: () => {\n record(status, status, \"watchdog.reconnect\");\n connection?.disconnect(true);\n },\n });\n\n const handle: DependencyHandle = {\n async connect(signal) {\n closing = false;\n const c = await create(connectionOptions);\n connection = c;\n watch(c);\n if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);\n // a server that is down at the start does not stop the startup: the client connects again in\n // the background, and its checks keep readiness tripped until it is ready (readiness is earned)\n await c.connect().catch((error: unknown) => record(status, status, \"connect.failed\", message(error)));\n watchdog?.stop();\n watchdog = startWatchdog();\n },\n async close() {\n closing = true;\n watchdog?.stop();\n watchdog = null;\n const c = connection;\n if (c === null) return;\n // a QUIT lets the replies in flight arrive. A silent server never answers it, thus the close has its bound\n const startedClose = clock.now();\n if (!(await withinBound(clock, ms(knobs.close_bound), c.quit()))) record(status, status, \"close.bound\");\n // a client that is not ready refuses the QUIT at once, and it would connect again: the close stops it\n if (c.status !== \"end\") c.disconnect(false);\n // ioredis is at `end` when its socket closes, a moment after the reply: the close waits for it, in its bound\n const ended = (within: number): Promise<void> =>\n c.status === \"end\"\n ? Promise.resolve()\n : new Promise<void>((resolve) => {\n let stop: Cancel = () => {};\n const end = (): void => {\n c.off(\"end\", end);\n stop();\n resolve();\n };\n c.on(\"end\", end);\n stop = clock.after(Math.max(0, within), end);\n });\n await ended(ms(knobs.close_bound) - (clock.now() - startedClose));\n // a disconnect only half-closes the socket, and a silent server never closes its half: the bound destroys it\n if (c.status !== \"end\") {\n record(status, status, \"close.destroy\");\n c.stream?.destroy?.();\n await ended(0);\n }\n for (const [event, listener] of listeners) c.off(event, listener);\n listeners.length = 0;\n flight.settleAll(\"the client closed with the command in flight\");\n status = \"end\";\n },\n };\n\n const declaration: Omit<Dependency, \"name\"> = {\n system: spec.system,\n knobs: {\n connect_bound: knobs.connect_bound,\n request_timeout: knobs.request_timeout,\n driver_writeoff_time: knobs.driver_writeoff_time,\n keepalive_idle: knobs.keepalive_idle,\n heartbeat_interval: knobs.heartbeat_interval,\n heartbeat_timeout: knobs.heartbeat_timeout,\n heartbeat_detection_time: detection,\n client_retry_window: knobs.client_retry_window,\n retry_base: knobs.retry_base,\n retry_cap: knobs.retry_cap,\n close_bound: knobs.close_bound,\n max_connection_age: knobs.max_connection_age,\n retrans_giveup_time: knobs.retrans_giveup_time,\n },\n operations,\n };\n\n const checks = (checkOptions: CheckOptions) =>\n stepChecks<ClientStep>(name, checkOptions, {\n // the state of the client, without a round trip\n \"local-state\": localStateCheck(() => connection?.status === \"ready\"),\n // a PING on the connection of the application: blind to nothing that the application meets\n \"round-trip\": roundTripCheck((sendOptions) => send(\"PING\", [], sendOptions)),\n // a new connection each time: it finds a server that refuses connections, but not a stalled connection of the application\n dial: dialCheck(dial, spec.host, spec.port),\n });\n\n return {\n name,\n declaration,\n handle,\n meets: meetings(name, knobs, detection),\n get status() {\n return connection === null ? \"none\" : connection.status;\n },\n joins,\n send,\n checks,\n attach: (svc, checkOptions) => attachClient(svc, name, declaration, handle, checks(checkOptions)),\n problems: (description) => clientProblems(name, description, clientBounds(knobs)),\n };\n};\n","import type { Clock } from \"../clock.ts\";\nimport type { Decision } from \"../limiter/policy.ts\";\nimport type { LimiterStore } from \"../limiter/store.ts\";\nimport type { RedisClient } from \"./client.ts\";\n\n/**\n * This script decides one take in one atomic step of Redis. It does the arithmetic of `decide`, and\n * it reads `TIME` of the server, the one clock of all replicas. A test can give the time in its place.\n * - KEYS[1] is the state of the key. KEYS[2] is the index of the limiter, a sorted set of its keys\n * by the time at which each key is free.\n * - ARGV is the type of the rule, its two numbers, the cap of keys, the key, and the time or \"\".\n * - The state of a bucket is its full time. The state of a window is the time of each take in it.\n * - The state expires at its free time (PX), and the index lets the cap count only busy keys.\n * - The reply is `{1, remaining}`, or `{0, retryAfter, cause}` with the time as text. Redis gives\n * each Lua number as a whole number, thus the fraction of a millisecond goes as text.\n */\nexport const LIMITER_SCRIPT = `\nlocal kind = ARGV[1]\nlocal a = tonumber(ARGV[2])\nlocal b = tonumber(ARGV[3])\nlocal cap = tonumber(ARGV[4])\nlocal member = ARGV[5]\nlocal now\nif ARGV[6] == '' then\n local t = redis.call('TIME')\n now = tonumber(t[1]) * 1000 + tonumber(t[2]) / 1000\nelse\n now = tonumber(ARGV[6])\nend\nlocal TOL = 1e-6\nlocal function num(x) return string.format('%.6f', x) end\nlocal raw = redis.call('GET', KEYS[1])\nlocal full = nil\nlocal stamps = nil\nif raw then\n if kind == 'token-bucket' then\n full = tonumber(raw)\n else\n stamps = {}\n for x in string.gmatch(raw, '[^,]+') do stamps[#stamps + 1] = tonumber(x) end\n end\nend\nlocal function freeAt()\n if kind == 'token-bucket' then return full end\n if #stamps == 0 then return -math.huge end\n return stamps[#stamps] + b\nend\nif raw and freeAt() <= now then\n redis.call('DEL', KEYS[1])\n redis.call('ZREM', KEYS[2], member)\n raw = nil\n full = nil\n stamps = nil\nend\nif not raw then\n if redis.call('ZCARD', KEYS[2]) >= cap then\n redis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', num(now))\n if redis.call('ZCARD', KEYS[2]) >= cap then\n local first = redis.call('ZRANGE', KEYS[2], 0, 0, 'WITHSCORES')\n return {0, num(tonumber(first[2]) - now), 'cap'}\n end\n end\nend\nlocal remaining\nlocal state\nif kind == 'token-bucket' then\n local span = a * b\n local base = now\n if full and full > now then base = full end\n local nxt = base + a\n if nxt - now > span + TOL then\n return {0, num(nxt - span - now), 'limit'}\n end\n remaining = math.floor((span - (nxt - now)) / a + TOL)\n if remaining < 0 then remaining = 0 end\n full = nxt\n state = num(full)\nelse\n local live = {}\n for _, stamp in ipairs(stamps or {}) do\n if stamp + b > now + TOL then live[#live + 1] = stamp end\n end\n if #live >= a then\n return {0, num(live[1] + b - now), 'limit'}\n end\n live[#live + 1] = now\n remaining = a - #live\n stamps = live\n local parts = {}\n for i, stamp in ipairs(live) do parts[i] = num(stamp) end\n state = table.concat(parts, ',')\nend\nlocal free = freeAt()\nlocal ttl = math.ceil(free - now)\nif ttl < 1 then ttl = 1 end\nredis.call('SET', KEYS[1], state, 'PX', ttl)\nredis.call('ZADD', KEYS[2], num(free), member)\nif redis.call('PTTL', KEYS[2]) < ttl then redis.call('PEXPIRE', KEYS[2], ttl) end\nreturn {1, remaining}\n`;\n\n/** These options make a store of a limiter in Redis. */\nexport interface RedisLimiterOptions {\n /** This text starts the name of each key of the store in Redis, for example `limit:`. It has no braces. */\n readonly prefix: string;\n /**\n * This clock replaces `TIME` of the server, for a test that compares the store with `decide`. Without\n * it, the script reads `TIME`, thus each replica gets the one clock of the store.\n */\n readonly clock?: Clock | undefined;\n}\n\n/** A Redis client part is the part of a Redis client that the store uses: `send`. */\nexport type RedisSender = Pick<RedisClient, \"send\">;\n\n/**\n * This function makes a store that the replicas share, in Redis 7. Each take is one `EVAL` of\n * `LIMITER_SCRIPT`. The client does not send it again, because a second take counts two times.\n * - The names of the keys have the limiter in braces, a hash tag. Thus the state and the index of a\n * limiter are in one slot of a Redis Cluster.\n * - Each state expires at the time that the key is free, thus the eviction is exact, as in the memory\n * store. The index of the limiter bounds the count of busy keys to `maxKeys`.\n * - A reply of an error, an outcome that is not certain, or a reply of another shape rejects the\n * take of the store. `keyedLimiter` then applies the `onStoreFailure` of the spec.\n * - `TIME` is the wall clock of the Redis host, not a monotonic clock. Thus a step of that clock\n * moves the counts of each key. The replicas still share one clock.\n */\nexport const redisLimiterStore = (client: RedisSender, options: RedisLimiterOptions): LimiterStore => {\n if (/[{}]/.test(options.prefix)) throw new Error(`the prefix ${options.prefix} of a limiter store has a brace: the store puts the hash tag in it`);\n return {\n kind: \"shared\",\n async take({ limiter, key, rule, maxKeys }): Promise<Decision> {\n const base = `${options.prefix}{${limiter}}`;\n const numbers = rule.type === \"token-bucket\" ? [rule.interval, rule.burst] : [rule.limit, rule.window];\n const now = options.clock === undefined ? \"\" : String(options.clock.now());\n const sent = await client.send<unknown[]>(\"EVAL\", [\n LIMITER_SCRIPT,\n 2,\n `${base}:key:${key}`,\n `${base}:keys`,\n rule.type,\n String(numbers[0]),\n String(numbers[1]),\n String(maxKeys),\n key,\n now,\n ]);\n if (sent.outcome !== \"ok\") throw new Error(`the limiter store ${limiter} failed (${sent.outcome}): ${sent.error}`);\n return replyOf(limiter, sent.value);\n },\n };\n};\n\n/** This function gives the decision of a reply of `LIMITER_SCRIPT`. A reply of another shape throws. */\nconst replyOf = (limiter: string, reply: unknown): Decision => {\n if (Array.isArray(reply) && Number(reply[0]) === 1 && reply.length === 2) return { allowed: true, remaining: Number(reply[1]) };\n if (Array.isArray(reply) && Number(reply[0]) === 0 && reply.length === 3 && (reply[2] === \"limit\" || reply[2] === \"cap\")) {\n const retryAfter = Number(reply[1]);\n if (Number.isFinite(retryAfter) && retryAfter > 0) return { allowed: false, retryAfter: { value: retryAfter, unit: \"ms\" }, cause: reply[2] };\n }\n throw new Error(`the limiter store ${limiter} gave a reply that is not a decision: ${JSON.stringify(reply)}`);\n};\n","import type { Machine } from \"@mark1russell7/algebra\";\nimport { clientKnob as knob, closeTimer, transition as t } from \"../client/machine.ts\";\n\n/** The states where the connection exists or tries to exist, thus where it can close. */\nconst LIVE = [\"connecting\", \"connect\", \"ready\", \"reconnecting\"] as const;\nconst ANY = [\"none\", \"connecting\", \"connect\", \"ready\", \"close\", \"reconnecting\", \"end\"] as const;\n\n/**\n * The Redis client as a `mark1russell7.machine/1` document. Its states are the states of ioredis.\n * The wrapper adds its own events: the join at each `ready`, and the misses of its watchdog. It also\n * adds a command that it sends again, and the bound of its close. A test records the transitions of the\n * client and replays them against this document.\n *\n * The state `ready` hides states that the client cannot see (D4). In a black hole, ioredis stays\n * `ready` while the kernel retransmits, until `driver_writeoff_time` or the watchdog ends that\n * hidden interval. A timer of this document bounds each hidden interval.\n */\nexport const REDIS_CLIENT_MACHINE: Machine = {\n format: \"mark1russell7.machine/1\",\n id: \"service.machine.redis-client\",\n title: \"The Redis client of the service, over ioredis 6\",\n layer: \"driver\",\n version: \"0.2.0\",\n sources: [{ ref: \"packages/service/src/redis (the client implements this document)\", rung: \"implemented\" }],\n states: {\n none: { initial: true, note: \"the client exists, and its connection does not\" },\n connecting: { note: \"a TCP connection attempt\" },\n connect: { note: \"the handshake: HELLO, then the ready check (INFO)\" },\n ready: { note: \"it can hide a black hole or a frozen server, until a timer of the client ends it\" },\n close: { note: \"the socket closed. Each command in flight settles as indeterminate\" },\n reconnecting: { note: \"the delay of the retry strategy, which doubles to its cap\" },\n end: { note: \"the retry window ended, or the service closed the client. The watchdog starts it again\" },\n },\n events: {\n \"status.connecting\": { kind: \"local\" },\n \"status.connect\": { kind: \"rx\" },\n \"status.ready\": { kind: \"rx\" },\n \"status.close\": { kind: \"rx\" },\n \"status.reconnecting\": { kind: \"timer\" },\n \"status.end\": { kind: \"local\" },\n join: { kind: \"rx\", note: \"the reply of CLIENT ID at ready (D1)\" },\n error: { kind: \"rx\" },\n \"connect.failed\": { kind: \"local\" },\n \"command.written\": { kind: \"tx\" },\n \"command.reissue\": { kind: \"local\" },\n data: { kind: \"rx\" },\n heartbeat: { kind: \"timer\" },\n \"watchdog.miss\": { kind: \"local\" },\n \"watchdog.reconnect\": { kind: \"local\" },\n \"watchdog.revive\": { kind: \"local\" },\n \"retry.window-ended\": { kind: \"local\" },\n \"close.request\": { kind: \"local\" },\n \"close.bound\": { kind: \"timer\" },\n \"close.destroy\": { kind: \"local\", note: \"the bound destroys a socket that a silent server does not close\" },\n },\n knobs: {\n connect_bound: knob(\"connectTimeout\"),\n driver_writeoff_time: knob(\"socketTimeout\"),\n heartbeat_interval: knob(\"watchdog.interval\"),\n close_bound: knob(\"close.bound\"),\n },\n timers: {\n connect_timeout: {\n duration: { knob: \"connect_bound\", unit: \"s\" },\n arms: [\"enter:connecting\"],\n cancels: [\"status.connect\", \"status.close\"],\n fires: \"status.close\",\n note: \"ioredis destroys the attempt\",\n },\n writeoff: {\n duration: { knob: \"driver_writeoff_time\", unit: \"s\" },\n arms: [\"command.written\"],\n cancels: [\"data\", \"status.close\"],\n fires: \"status.close\",\n note: \"ioredis destroys a socket that gives no data while a command waits: it ends the hidden interval of a black hole\",\n },\n heartbeat: {\n duration: { knob: \"heartbeat_interval\", unit: \"s\" },\n arms: [\"enter:ready\", \"heartbeat\"],\n cancels: [\"status.close\", \"status.end\"],\n fires: \"heartbeat\",\n note: \"a PING on the connection of the application\",\n },\n close: closeTimer(),\n },\n transitions: [\n t(\"connect\", [\"none\", \"reconnecting\", \"end\", \"close\"], \"status.connecting\", \"connecting\"),\n t(\"tcp\", \"connecting\", \"status.connect\", \"connect\"),\n t(\"ready\", \"connect\", \"status.ready\", \"ready\"),\n t(\"close\", [...LIVE], \"status.close\", \"close\"),\n t(\"retry\", \"close\", \"status.reconnecting\", \"reconnecting\"),\n t(\"end\", [...LIVE, \"close\"], \"status.end\", \"end\"),\n t(\"join\", \"ready\", \"join\", \"ready\"),\n t(\"miss\", \"ready\", \"watchdog.miss\", \"ready\"),\n t(\"watchdog-reconnect\", \"ready\", \"watchdog.reconnect\", \"ready\"),\n t(\"revive\", \"end\", \"watchdog.revive\", \"end\"),\n // the retry strategy answers in `close`, before ioredis moves to `reconnecting` or `end`\n t(\"window-ended\", \"close\", \"retry.window-ended\", \"close\"),\n ...ANY.map((state) => t(`error-${state}`, state, \"error\", state)),\n ...ANY.map((state) => t(`connect-failed-${state}`, state, \"connect.failed\", state)),\n ...ANY.map((state) => t(`reissue-${state}`, state, \"command.reissue\", state)),\n ...ANY.map((state) => t(`close-bound-${state}`, state, \"close.bound\", state, { timer: \"close\" })),\n ...ANY.map((state) => t(`close-destroy-${state}`, state, \"close.destroy\", state)),\n ],\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"redis.js","names":["knob","t"],"sources":["../src/redis/connection.ts","../src/redis/meets.ts","../src/redis/operations.ts","../src/redis/client.ts","../src/redis/limiter.ts","../src/redis/machine.ts"],"sourcesContent":["import type { ClientSocket, Factory } from \"../client/core.ts\";\n\n/**\n * A Redis connection is the part of an ioredis client that the wrapper uses. An ioredis 6 client has\n * each part. A fake for tests has them too. Thus the package depends on no type of ioredis.\n */\nexport interface RedisConnection {\n /** This text is the state of the client: wait, connecting, connect, ready, close, reconnecting or end. */\n readonly status: string;\n /** This socket is the socket of the current connection, when there is one. */\n readonly stream?: RedisSocket | undefined;\n connect(): Promise<void>;\n /** This method sends one command, and gives the reply. */\n call(command: string, ...args: Array<string | number>): Promise<unknown>;\n quit(): Promise<unknown>;\n /** This method closes the connection at once. With `true`, the client then connects again. */\n disconnect(reconnect?: boolean): void;\n on(event: string, listener: (...args: unknown[]) => void): unknown;\n off(event: string, listener: (...args: unknown[]) => void): unknown;\n}\n\n/** A Redis socket is the socket of a client in the core: the join of the prober and the bounded close read it. */\nexport type RedisSocket = ClientSocket;\n\n/**\n * These are the options that the wrapper gives ioredis. Each one comes from the declaration. The\n * wrapper sets each option that has an effect on a timer or on a send again, thus no default of\n * ioredis decides one.\n */\nexport interface RedisConnectionOptions {\n readonly host: string;\n readonly port: number;\n readonly db?: number | undefined;\n readonly username?: string | undefined;\n readonly password?: string | undefined;\n /** The wrapper connects in its own step of the startup. */\n readonly lazyConnect: true;\n readonly connectTimeout: number;\n readonly commandTimeout: number;\n /** This time destroys a socket that gives no data while a command waits. Undefined is no bound. */\n readonly socketTimeout: number | undefined;\n /** This time is the idle time of TCP keepalive. False is no keepalive. */\n readonly keepAlive: number | false;\n /** A command while the client is not ready fails at once: it did not reach the server. */\n readonly enableOfflineQueue: false;\n /** The client does not send again a command that can have reached the server. */\n readonly autoResendUnfulfilledCommands: false;\n readonly maxRetriesPerRequest: null;\n readonly enableReadyCheck: true;\n readonly connectionName: string;\n /** This function gives the delay before the next try to connect, or null to stop. */\n readonly retryStrategy: (times: number) => number | null;\n}\n\n/** A connection factory makes the connection of the client from its options. */\nexport type ConnectionFactory = Factory<RedisConnectionOptions, RedisConnection>;\n\n/**\n * This factory makes an ioredis client. It loads ioredis only when a client connects, thus a\n * service that does not use Redis does not need ioredis.\n */\nexport const ioredis: ConnectionFactory = async (options) => {\n const loaded = (await import(\"ioredis\")) as unknown as {\n readonly default: new (options: RedisConnectionOptions) => RedisConnection;\n };\n return new loaded.default(options);\n};\n","import type { Extent } from \"@mark1russell7/quantity\";\nimport type { Detector, Meeting, Situation } from \"../client/meets.ts\";\nimport { WHAT, immediate, keepaliveAnswered, keepaliveReset, keepaliveSilent, kernelGiveUp } from \"../client/meets.ts\";\nimport type { RedisKnobs } from \"./client.ts\";\n\n// the shapes and the queries moved to the core of the clients: this entry keeps them for the users of 0.5\nexport type { Detector, Meeting, Situation } from \"../client/meets.ts\";\nexport { firstDetector, unwatched } from \"../client/meets.ts\";\n\n/**\n * This function gives the manifestations that a Redis client can meet, and the detectors of each, from\n * its knobs. A reset or a FIN arrives on the next segment. A black hole and a host that is gone give\n * no answer at all. A frozen process and a stalled server keep a kernel that acknowledges each\n * segment. Thus TCP keepalive does not fire on them, and only the client's own timers find them.\n */\nexport const meetings = (name: string, knobs: RedisKnobs, heartbeat: Extent): Meeting[] => {\n const watchdog = (situation: Situation): Detector => ({\n by: \"heartbeat_detection_time\",\n within: heartbeat,\n situation,\n effect: `the watchdog of ${name} closes the connection, and the client connects again`,\n });\n const writeoff: Detector = {\n by: \"driver_writeoff_time\",\n within: knobs.driver_writeoff_time,\n situation: \"in flight\",\n effect: \"ioredis destroys the socket of a command without data, and the client connects again\",\n };\n const command: Detector = {\n by: \"request_timeout\",\n within: knobs.request_timeout,\n situation: \"in flight\",\n effect: \"the command fails as indeterminate, and the state of the connection does not change\",\n };\n const giveUp = kernelGiveUp(knobs.retrans_giveup_time);\n const silent = (manifestation: \"blackhole\" | \"freeze-host\"): Meeting => ({\n manifestation,\n what: WHAT[manifestation],\n detectors: [command, writeoff, watchdog(\"in flight\"), giveUp, watchdog(\"idle\"), keepaliveSilent(knobs.keepalive_idle)],\n });\n const acknowledged = (manifestation: string, what: string): Meeting => ({\n manifestation,\n what,\n detectors: [command, writeoff, watchdog(\"in flight\"), watchdog(\"idle\"), keepaliveAnswered()],\n });\n return [\n {\n manifestation: \"rst\",\n what: WHAT.rst,\n detectors: [\n immediate(\"in flight\", \"the command fails, and the client connects again, and each attempt is refused\"),\n watchdog(\"idle\"),\n keepaliveReset(knobs.keepalive_idle),\n ],\n },\n {\n manifestation: \"fin\",\n what: WHAT.fin,\n detectors: [immediate(\"in flight\", \"the close arrives, and the command fails\"), immediate(\"idle\", \"the close arrives\")],\n },\n silent(\"blackhole\"),\n silent(\"freeze-host\"),\n acknowledged(\"freeze-process\", WHAT[\"freeze-process\"]),\n acknowledged(\"app-stall\", \"the server stops its replies for a time, for example a long command, and its kernel stays up\"),\n ];\n};\n","import type { Operation } from \"../operation.ts\";\n\n/**\n * These are the Redis commands whose idempotency the client declares when the declaration does not\n * say. A second issue of an idempotent command has the same effect. Thus the client can send it\n * again after a failure that left its result unknown. The client does not send again a command\n * that is not in this table: a declaration that uses it says its idempotency itself.\n */\nconst SAME_EFFECT = [\n // reads\n \"PING\",\n \"ECHO\",\n \"GET\",\n \"MGET\",\n \"STRLEN\",\n \"GETRANGE\",\n \"EXISTS\",\n \"TYPE\",\n \"TTL\",\n \"PTTL\",\n \"EXPIRETIME\",\n \"HGET\",\n \"HMGET\",\n \"HGETALL\",\n \"HKEYS\",\n \"HVALS\",\n \"HLEN\",\n \"HEXISTS\",\n \"SMEMBERS\",\n \"SISMEMBER\",\n \"SMISMEMBER\",\n \"SCARD\",\n \"ZRANGE\",\n \"ZRANGEBYSCORE\",\n \"ZSCORE\",\n \"ZMSCORE\",\n \"ZRANK\",\n \"ZCARD\",\n \"ZCOUNT\",\n \"LRANGE\",\n \"LINDEX\",\n \"LLEN\",\n \"XRANGE\",\n \"XREVRANGE\",\n \"XLEN\",\n \"SCAN\",\n \"SSCAN\",\n \"HSCAN\",\n \"ZSCAN\",\n \"DBSIZE\",\n \"INFO\",\n \"CLIENT ID\",\n // writes that put a state, not a change of a state: a second issue puts the same state\n \"SET\",\n \"MSET\",\n \"SETEX\",\n \"PSETEX\",\n \"DEL\",\n \"UNLINK\",\n \"EXPIRE\",\n \"PEXPIRE\",\n \"EXPIREAT\",\n \"PEXPIREAT\",\n \"PERSIST\",\n \"HSET\",\n \"HDEL\",\n \"SADD\",\n \"SREM\",\n \"ZADD\",\n \"ZREM\",\n];\n\n/**\n * These commands change a state relative to the state before them. A second issue changes it again:\n * an INCR that the client sends again after a timeout counts two times.\n */\nconst CHANGE = [\n \"INCR\",\n \"INCRBY\",\n \"INCRBYFLOAT\",\n \"DECR\",\n \"DECRBY\",\n \"HINCRBY\",\n \"HINCRBYFLOAT\",\n \"ZINCRBY\",\n \"APPEND\",\n \"LPUSH\",\n \"RPUSH\",\n \"LPOP\",\n \"RPOP\",\n \"BLPOP\",\n \"BRPOP\",\n \"LMOVE\",\n \"BLMOVE\",\n \"SPOP\",\n \"XADD\",\n \"XACK\",\n \"PUBLISH\",\n \"EVAL\",\n \"EVALSHA\",\n \"FCALL\",\n \"MULTI\",\n \"EXEC\",\n \"GETDEL\",\n \"GETSET\",\n \"SETNX\",\n];\n\n/** These are the operations of the client: the idempotency of each command that the table names. */\nexport const REDIS_OPERATIONS: Readonly<Record<string, Operation>> = Object.fromEntries([\n ...SAME_EFFECT.map((command) => [command, { idempotent: true }] as const),\n ...CHANGE.map((command) => [command, { idempotent: false }] as const),\n]);\n\n/** This function gives the name of a command as the table writes it: upper case, and CLIENT ID with its subcommand. */\nexport const commandName = (command: string, args: readonly unknown[]): string => {\n const name = command.toUpperCase();\n if ((name === \"CLIENT\" || name === \"CONFIG\" || name === \"DEBUG\") && typeof args[0] === \"string\")\n return `${name} ${args[0].toUpperCase()}`;\n return name;\n};\n","import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { backoffDelay } from \"../backoff.ts\";\nimport type { Cancel } from \"../clock.ts\";\nimport { realClock } from \"../clock.ts\";\nimport type { Attempt, CheckOptions, Client, ClientJoin, ClientKnobs, ClientOptions, ClientSpec, ClientStep, DialFn, RetryKnobs, SendOptions, Sent } from \"../client/core.ts\";\nimport {\n attachClient,\n clientBounds,\n clientProblems,\n clientRecorder,\n dialCheck,\n finiteMs,\n inFlight,\n localStateCheck,\n message,\n missesProblems,\n ms,\n notSent,\n operationTable,\n readyWait,\n retryBackoff,\n retryProblems,\n roundTripCheck,\n sendWithReissue,\n socketEnds,\n stepChecks,\n timeProblems,\n withinBound,\n} from \"../client/core.ts\";\nimport { helloDial } from \"../client/dial.ts\";\nimport type { Dependency } from \"../description.ts\";\nimport type { Heartbeat } from \"../heartbeat.ts\";\nimport { heartbeat } from \"../heartbeat.ts\";\nimport type { DependencyHandle } from \"../lifecycle.ts\";\nimport { heartbeatDetection } from \"../timing.ts\";\nimport type { ConnectionFactory, RedisConnection, RedisConnectionOptions } from \"./connection.ts\";\nimport { ioredis } from \"./connection.ts\";\nimport { meetings } from \"./meets.ts\";\nimport { REDIS_OPERATIONS, commandName } from \"./operations.ts\";\n\n// the three outcomes, the send options and the checks moved to the core of the clients: this entry keeps them for the users of 0.5\nexport type { CheckOptions, ClientStep, DialFn, SendOptions, Sent } from \"../client/core.ts\";\n// the backoff of the tries to connect moved to the core, for RabbitMQ too: this entry keeps it for the users of 0.5\nexport { retryBackoff } from \"../client/core.ts\";\n\n/**\n * These are the knobs of the client, each a time, by the names of the ordering law where the law\n * has them. The runtime has no defaults, thus each knob is necessary, and a bound that nobody gives\n * is \"infinite\", not zero. The knobs that each client has come from the core.\n */\nexport interface RedisKnobs extends ClientKnobs, RetryKnobs {\n /** This time bounds a try to connect, until the TCP connection exists (`connectTimeout`). */\n readonly connect_bound: Quantity;\n /** This time bounds each command, the handshake commands too (`commandTimeout`). */\n readonly request_timeout: Quantity;\n /** This time destroys a socket that gives no data while a command waits (`socketTimeout`). */\n readonly driver_writeoff_time: Extent;\n /** This time is the interval of the in-band watchdog: a PING on the connection of the application. */\n readonly heartbeat_interval: Extent;\n /** This time bounds each PING of the watchdog. */\n readonly heartbeat_timeout: Quantity;\n /** This time is how long the client tries to connect again before it stops. The watchdog then starts it again. */\n readonly client_retry_window: Extent;\n /** This time is the first delay before a new try to connect. The delay doubles after each try. */\n readonly retry_base: Quantity;\n /** This time is the most that one delay can be, before its jitter. */\n readonly retry_cap: Quantity;\n /** This time bounds the close of the client at the shutdown: a QUIT, then a disconnect. */\n readonly close_bound: Quantity;\n /** This time is the age at which the client replaces its connection. The client supports only \"infinite\". */\n readonly max_connection_age: Extent;\n}\n\n/** A Redis client spec is the declaration of one Redis client: where it connects, and its knobs. */\nexport interface RedisClientSpec extends ClientSpec<RedisKnobs> {\n readonly db?: number | undefined;\n readonly username?: string | undefined;\n /** This count is the number of PINGs in sequence without an answer that make the watchdog connect again. */\n readonly heartbeatMisses: number;\n}\n\n/** These options make a client. Without them, it uses ioredis, the real clock and `Math.random`. */\nexport interface RedisClientOptions extends ClientOptions<ConnectionFactory> {\n readonly random?: (() => number) | undefined;\n /**\n * This function waits one turn of the event loop. The watchdog waits it before it counts a miss,\n * thus a reply that is in the socket is read first. Without it, the client uses `setImmediate`.\n */\n readonly turn?: (() => Promise<void>) | undefined;\n}\n\n/**\n * A join is one generation of the connection, at its `ready` (D1, D2). The prober joins its kernel\n * records to the server's view of the connection through it. A join holds the server's id of the\n * connection, and the local address and port of the socket.\n */\nexport interface Join extends ClientJoin {\n /** This number is the reply of CLIENT ID, or null when the server gave none. */\n readonly clientId: number | null;\n}\n\n/** A Redis client is a dependency of a service: its declaration, its handle, its checks and its commands. */\nexport interface RedisClient extends Client<ClientStep, Join> {\n /** This text is the state of the connection, as ioredis names it, or \"none\" before the connect. */\n readonly status: string;\n /**\n * This method sends a command, and gives its outcome in three values. The client sends again\n * only an idempotent operation, one time at most, after an outcome that is not certain. The\n * second try stays in the bound of the request. The client does not send again an operation that\n * the table does not name.\n */\n send<T = unknown>(command: string, args?: ReadonlyArray<string | number>, options?: SendOptions): Promise<Sent<T>>;\n}\n\n/** This function gives the problems of a spec: the knobs that the client cannot use. */\nexport const specProblems = (name: string, spec: RedisClientSpec): string[] => {\n const problems: string[] = [];\n const { knobs } = spec;\n problems.push(...missesProblems(name, spec.heartbeatMisses, \"watchdog\"));\n if (knobs.max_connection_age.value !== \"infinite\")\n problems.push(`${name} cannot replace its connection at an age: max_connection_age must be \"infinite\"`);\n problems.push(...timeProblems(name, knobs));\n if (ms(knobs.request_timeout) <= 0) problems.push(`${name}'s request_timeout is not above zero: each command must have a bound`);\n if (ms(knobs.heartbeat_timeout) <= 0) problems.push(`${name}'s heartbeat_timeout is not above zero`);\n problems.push(...retryProblems(name, knobs));\n return problems;\n};\n\n/** This dial function opens a TCP connection, sends an inline PING, and waits for +PONG. */\nexport const tcpDial: DialFn = helloDial(\"PING\\r\\n\", (received) => {\n const reply = received.toString();\n if (!reply.includes(\"\\r\\n\")) return \"more\";\n return reply.startsWith(\"+PONG\") ? null : new Error(`the reply was ${reply.trim()}`);\n});\n\n/**\n * This function makes a Redis client. It is a dependency of a service, with ioredis under it:\n * - It sets each timer of ioredis from the declaration, and it does not let ioredis send a command\n * again (R5 §8).\n * - It tells the outcome of a command in three values, and it settles each command in flight as\n * indeterminate when its connection closes (C8, C10).\n * - It records the server's id of each connection at `ready` (D1).\n * - A watchdog sends a PING in the band of the connection, and it connects again after the misses\n * of the declaration. It also starts a client that stopped (principle 19).\n *\n * The outcomes, the second try, the commands in flight, the bounded close and the checks come\n * from the core of the clients. This entry gives what ioredis does in its own way.\n */\nexport const redisClient = (name: string, spec: RedisClientSpec, options: RedisClientOptions = {}): RedisClient => {\n const problems = specProblems(name, spec);\n if (problems.length > 0) throw new Error(problems.join(\"; \"));\n const { knobs } = spec;\n const clock = options.clock ?? realClock;\n const random = options.random ?? Math.random;\n const create = options.create ?? ioredis;\n const dial = options.dial ?? tcpDial;\n const turn = options.turn ?? (() => new Promise<void>((resolve) => setImmediate(resolve)));\n const operations = operationTable(REDIS_OPERATIONS, spec.operations);\n const requestTimeout = ms(knobs.request_timeout);\n const detection = heartbeatDetection(knobs.heartbeat_interval, knobs.heartbeat_timeout, spec.heartbeatMisses);\n const backoff = retryBackoff(knobs);\n\n let connection: RedisConnection | null = null;\n let status = \"none\";\n let closing = false;\n const joins: Join[] = [];\n // the commands that the client wrote while the connection was ready: a close settles each one\n const flight = inFlight();\n // the commands that wait for a ready connection before their second try\n const readied = readyWait(clock, () => connection?.status === \"ready\");\n const listeners: Array<[string, (...args: unknown[]) => void]> = [];\n\n const record = clientRecorder(options.record, clock, name);\n\n // the delays of the tries to connect (`retryBackoff`), and none after the retry window. The\n // watchdog starts the client again after that\n let retrySince: number | null = null;\n const retryStrategy = (times: number): number | null => {\n const now = clock.now();\n retrySince ??= now;\n if (now - retrySince > ms(knobs.client_retry_window)) {\n record(status, status, \"retry.window-ended\");\n return null;\n }\n return Math.round(backoffDelay(backoff, times, random));\n };\n\n const connectionOptions: RedisConnectionOptions = {\n host: spec.host,\n port: spec.port,\n ...(spec.db === undefined ? {} : { db: spec.db }),\n ...(spec.username === undefined ? {} : { username: spec.username }),\n ...(spec.password === undefined ? {} : { password: spec.password }),\n lazyConnect: true,\n connectTimeout: ms(knobs.connect_bound),\n commandTimeout: requestTimeout,\n // ioredis reads undefined as no socket timeout, and false as no keepalive\n socketTimeout: finiteMs(knobs.driver_writeoff_time),\n keepAlive: finiteMs(knobs.keepalive_idle) ?? false,\n enableOfflineQueue: false,\n autoResendUnfulfilledCommands: false,\n maxRetriesPerRequest: null,\n enableReadyCheck: true,\n connectionName: name,\n retryStrategy,\n };\n\n /** This function gives the outcome of a failed command: a reply of the server, a command that did not leave the client, or neither. */\n const failed = (error: unknown): Attempt => {\n const text = message(error);\n if (error instanceof Error && error.name === \"ReplyError\") return { outcome: \"error\", error: text, reply: true };\n if (/isn't writeable|enableOfflineQueue/i.test(text)) return notSent(text);\n return { outcome: \"indeterminate\", error: text };\n };\n\n const onReady = async (): Promise<void> => {\n retrySince = null;\n const c = connection;\n if (c === null) return;\n const generation = joins.length + 1;\n const at = clock.now();\n const socket = c.stream;\n let clientId: number | null = null;\n try {\n const reply = await c.call(\"CLIENT\", \"ID\");\n clientId = typeof reply === \"number\" ? reply : Number(reply);\n if (!Number.isFinite(clientId)) clientId = null;\n } catch {\n clientId = null;\n }\n // a connection that closed before its reply is a generation that never served\n if (connection !== c || c.status !== \"ready\") return;\n joins.push({ generation, at, clientId, ...socketEnds(socket) });\n record(\"ready\", \"ready\", \"join\", `generation ${generation}, client id ${clientId ?? \"unknown\"}`);\n readied.wake();\n };\n\n const onStatus = (to: string) => (): void => {\n const from = status;\n status = to;\n record(from, to, `status.${to}`);\n // the lower layer gave up the connection: each command in flight learns it now (C10)\n if (to === \"close\" || to === \"end\") flight.settleAll(`the connection closed with the command in flight (${from})`);\n if (to === \"ready\") void onReady();\n };\n\n const watch = (c: RedisConnection): void => {\n for (const state of [\"connecting\", \"connect\", \"ready\", \"close\", \"reconnecting\", \"end\"]) {\n const listener = onStatus(state);\n c.on(state, listener);\n listeners.push([state, listener]);\n }\n // an error of the connection comes before its close, which settles the commands: here it is only recorded\n const onError = (error: unknown): void => record(status, status, \"error\", message(error));\n c.on(\"error\", onError);\n listeners.push([\"error\", onError]);\n };\n\n const attempt = (command: string, args: ReadonlyArray<string | number>, signal?: AbortSignal): Promise<Attempt> => {\n const c = connection;\n if (c === null) return Promise.resolve(notSent(`${name} has not connected`));\n // ioredis has no offline queue here: a command while the client is not ready fails at once, before it leaves the client\n if (c.status !== \"ready\") return Promise.resolve(notSent(`${name} is ${c.status}, not ready: the command did not leave the client`));\n if (signal?.aborted) return Promise.resolve(notSent(\"the caller aborted the command before it left the client\"));\n return flight.track(signal, \"the caller abandoned the command after it was sent\", (settle) => {\n c.call(command, ...args).then(\n (value) => settle({ outcome: \"ok\", value }),\n (error: unknown) => settle(failed(error)),\n );\n });\n };\n\n const send = <T>(command: string, args: ReadonlyArray<string | number> = [], sendOptions: SendOptions = {}): Promise<Sent<T>> =>\n sendWithReissue({\n clock,\n bound: requestTimeout,\n operation: operations[commandName(command, args)],\n options: sendOptions,\n attempt: (signal) => attempt(command, args, signal) as Promise<Attempt<T>>,\n // ioredis connects again by itself: the second try waits for a ready connection, in the time that is left\n beforeAgain: (left, signal) => readied.wait(left, signal),\n onReissue: () => record(status, status, \"command.reissue\", commandName(command, args)),\n });\n\n // the watchdog: a heartbeat of PINGs on the connection of the application, each interval from the\n // start of the one before. A reply, an error reply too, is an answer. It waits one turn of the\n // event loop before it counts a miss, thus a slow loop is not a silent server. It acts only on a\n // ready connection, thus it never closes a connection in the middle of a new attempt\n let watchdog: Heartbeat | null = null;\n const startWatchdog = (): Heartbeat =>\n heartbeat({\n interval: ms(knobs.heartbeat_interval),\n timeout: ms(knobs.heartbeat_timeout),\n misses: spec.heartbeatMisses,\n clock,\n turn,\n // a PING starts at each tick, also while an earlier PING waits\n overlap: true,\n due: () => {\n const c = connection;\n if (c === null || closing) return false;\n if (c.status === \"end\") {\n // the retry window ended: the watchdog starts the client again\n record(status, status, \"watchdog.revive\");\n retrySince = null;\n c.connect().catch(() => {});\n return false;\n }\n return c.status === \"ready\";\n },\n ping: (signal) =>\n attempt(\"PING\", [], signal).then((result) => result.outcome === \"ok\" || (result.outcome === \"error\" && result.reply)),\n live: () => connection?.status === \"ready\" && !closing,\n onMiss: (misses) => record(status, status, \"watchdog.miss\", `${misses} of ${spec.heartbeatMisses}`),\n onDead: () => {\n record(status, status, \"watchdog.reconnect\");\n connection?.disconnect(true);\n },\n });\n\n const handle: DependencyHandle = {\n async connect(signal) {\n closing = false;\n const c = await create(connectionOptions);\n connection = c;\n watch(c);\n if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);\n // a server that is down at the start does not stop the startup: the client connects again in\n // the background, and its checks keep readiness tripped until it is ready (readiness is earned)\n await c.connect().catch((error: unknown) => record(status, status, \"connect.failed\", message(error)));\n watchdog?.stop();\n watchdog = startWatchdog();\n },\n async close() {\n closing = true;\n watchdog?.stop();\n watchdog = null;\n const c = connection;\n if (c === null) return;\n // a QUIT lets the replies in flight arrive. A silent server never answers it, thus the close has its bound\n const startedClose = clock.now();\n if (!(await withinBound(clock, ms(knobs.close_bound), c.quit()))) record(status, status, \"close.bound\");\n // a client that is not ready refuses the QUIT at once, and it would connect again: the close stops it\n if (c.status !== \"end\") c.disconnect(false);\n // ioredis is at `end` when its socket closes, a moment after the reply: the close waits for it, in its bound\n const ended = (within: number): Promise<void> =>\n c.status === \"end\"\n ? Promise.resolve()\n : new Promise<void>((resolve) => {\n let stop: Cancel = () => {};\n const end = (): void => {\n c.off(\"end\", end);\n stop();\n resolve();\n };\n c.on(\"end\", end);\n stop = clock.after(Math.max(0, within), end);\n });\n await ended(ms(knobs.close_bound) - (clock.now() - startedClose));\n // a disconnect only half-closes the socket, and a silent server never closes its half: the bound destroys it\n if (c.status !== \"end\") {\n record(status, status, \"close.destroy\");\n c.stream?.destroy?.();\n await ended(0);\n }\n for (const [event, listener] of listeners) c.off(event, listener);\n listeners.length = 0;\n flight.settleAll(\"the client closed with the command in flight\");\n status = \"end\";\n },\n };\n\n const declaration: Omit<Dependency, \"name\"> = {\n system: spec.system,\n knobs: {\n connect_bound: knobs.connect_bound,\n request_timeout: knobs.request_timeout,\n driver_writeoff_time: knobs.driver_writeoff_time,\n keepalive_idle: knobs.keepalive_idle,\n heartbeat_interval: knobs.heartbeat_interval,\n heartbeat_timeout: knobs.heartbeat_timeout,\n heartbeat_detection_time: detection,\n client_retry_window: knobs.client_retry_window,\n retry_base: knobs.retry_base,\n retry_cap: knobs.retry_cap,\n close_bound: knobs.close_bound,\n max_connection_age: knobs.max_connection_age,\n retrans_giveup_time: knobs.retrans_giveup_time,\n },\n operations,\n };\n\n const checks = (checkOptions: CheckOptions) =>\n stepChecks<ClientStep>(name, checkOptions, {\n // the state of the client, without a round trip\n \"local-state\": localStateCheck(() => connection?.status === \"ready\"),\n // a PING on the connection of the application: blind to nothing that the application meets\n \"round-trip\": roundTripCheck((sendOptions) => send(\"PING\", [], sendOptions)),\n // a new connection each time: it finds a server that refuses connections, but not a stalled connection of the application\n dial: dialCheck(dial, spec.host, spec.port),\n });\n\n return {\n name,\n declaration,\n handle,\n meets: meetings(name, knobs, detection),\n get status() {\n return connection === null ? \"none\" : connection.status;\n },\n joins,\n send,\n checks,\n attach: (svc, checkOptions) => attachClient(svc, name, declaration, handle, checks(checkOptions)),\n problems: (description) => clientProblems(name, description, clientBounds(knobs)),\n };\n};\n","import type { Clock } from \"../clock.ts\";\nimport type { Decision } from \"../limiter/policy.ts\";\nimport type { LimiterStore } from \"../limiter/store.ts\";\nimport type { RedisClient } from \"./client.ts\";\n\n/**\n * This script decides one take in one atomic step of Redis. It does the arithmetic of `decide`, and\n * it reads `TIME` of the server, the one clock of all replicas. A test can give the time in its place.\n * - KEYS[1] is the state of the key. KEYS[2] is the index of the limiter, a sorted set of its keys\n * by the time at which each key is free.\n * - ARGV is the type of the rule, its two numbers, the cap of keys, the key, and the time or \"\".\n * - The state of a bucket is its full time. The state of a window is the time of each take in it.\n * - The state expires at its free time (PX), and the index lets the cap count only busy keys.\n * - The reply is `{1, remaining}`, or `{0, retryAfter, cause}` with the time as text. Redis gives\n * each Lua number as a whole number, thus the fraction of a millisecond goes as text.\n */\nexport const LIMITER_SCRIPT = `\nlocal kind = ARGV[1]\nlocal a = tonumber(ARGV[2])\nlocal b = tonumber(ARGV[3])\nlocal cap = tonumber(ARGV[4])\nlocal member = ARGV[5]\nlocal now\nif ARGV[6] == '' then\n local t = redis.call('TIME')\n now = tonumber(t[1]) * 1000 + tonumber(t[2]) / 1000\nelse\n now = tonumber(ARGV[6])\nend\nlocal TOL = 1e-6\nlocal function num(x) return string.format('%.6f', x) end\nlocal raw = redis.call('GET', KEYS[1])\nlocal full = nil\nlocal stamps = nil\nif raw then\n if kind == 'token-bucket' then\n full = tonumber(raw)\n else\n stamps = {}\n for x in string.gmatch(raw, '[^,]+') do stamps[#stamps + 1] = tonumber(x) end\n end\nend\nlocal function freeAt()\n if kind == 'token-bucket' then return full end\n if #stamps == 0 then return -math.huge end\n return stamps[#stamps] + b\nend\nif raw and freeAt() <= now then\n redis.call('DEL', KEYS[1])\n redis.call('ZREM', KEYS[2], member)\n raw = nil\n full = nil\n stamps = nil\nend\nif not raw then\n if redis.call('ZCARD', KEYS[2]) >= cap then\n redis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', num(now))\n if redis.call('ZCARD', KEYS[2]) >= cap then\n local first = redis.call('ZRANGE', KEYS[2], 0, 0, 'WITHSCORES')\n return {0, num(tonumber(first[2]) - now), 'cap'}\n end\n end\nend\nlocal remaining\nlocal state\nif kind == 'token-bucket' then\n local span = a * b\n local base = now\n if full and full > now then base = full end\n local nxt = base + a\n if nxt - now > span + TOL then\n return {0, num(nxt - span - now), 'limit'}\n end\n remaining = math.floor((span - (nxt - now)) / a + TOL)\n if remaining < 0 then remaining = 0 end\n full = nxt\n state = num(full)\nelse\n local live = {}\n for _, stamp in ipairs(stamps or {}) do\n if stamp + b > now + TOL then live[#live + 1] = stamp end\n end\n if #live >= a then\n return {0, num(live[1] + b - now), 'limit'}\n end\n live[#live + 1] = now\n remaining = a - #live\n stamps = live\n local parts = {}\n for i, stamp in ipairs(live) do parts[i] = num(stamp) end\n state = table.concat(parts, ',')\nend\nlocal free = freeAt()\nlocal ttl = math.ceil(free - now)\nif ttl < 1 then ttl = 1 end\nredis.call('SET', KEYS[1], state, 'PX', ttl)\nredis.call('ZADD', KEYS[2], num(free), member)\nif redis.call('PTTL', KEYS[2]) < ttl then redis.call('PEXPIRE', KEYS[2], ttl) end\nreturn {1, remaining}\n`;\n\n/** These options make a store of a limiter in Redis. */\nexport interface RedisLimiterOptions {\n /** This text starts the name of each key of the store in Redis, for example `limit:`. It has no braces. */\n readonly prefix: string;\n /**\n * This clock replaces `TIME` of the server, for a test that compares the store with `decide`. Without\n * it, the script reads `TIME`, thus each replica gets the one clock of the store.\n */\n readonly clock?: Clock | undefined;\n}\n\n/** A Redis client part is the part of a Redis client that the store uses: `send`. */\nexport type RedisSender = Pick<RedisClient, \"send\">;\n\n/**\n * This function makes a store that the replicas share, in Redis 7. Each take is one `EVAL` of\n * `LIMITER_SCRIPT`. The client does not send it again, because a second take counts two times.\n * - The names of the keys have the limiter in braces, a hash tag. Thus the state and the index of a\n * limiter are in one slot of a Redis Cluster.\n * - Each state expires at the time that the key is free, thus the eviction is exact, as in the memory\n * store. The index of the limiter bounds the count of busy keys to `maxKeys`.\n * - A reply of an error, an outcome that is not certain, or a reply of another shape rejects the\n * take of the store. `keyedLimiter` then applies the `onStoreFailure` of the spec.\n * - `TIME` is the wall clock of the Redis host, not a monotonic clock. Thus a step of that clock\n * moves the counts of each key. The replicas still share one clock.\n */\nexport const redisLimiterStore = (client: RedisSender, options: RedisLimiterOptions): LimiterStore => {\n if (/[{}]/.test(options.prefix)) throw new Error(`the prefix ${options.prefix} of a limiter store has a brace: the store puts the hash tag in it`);\n return {\n kind: \"shared\",\n async take({ limiter, key, rule, maxKeys }): Promise<Decision> {\n const base = `${options.prefix}{${limiter}}`;\n const numbers = rule.type === \"token-bucket\" ? [rule.interval, rule.burst] : [rule.limit, rule.window];\n const now = options.clock === undefined ? \"\" : String(options.clock.now());\n const sent = await client.send<unknown[]>(\"EVAL\", [\n LIMITER_SCRIPT,\n 2,\n `${base}:key:${key}`,\n `${base}:keys`,\n rule.type,\n String(numbers[0]),\n String(numbers[1]),\n String(maxKeys),\n key,\n now,\n ]);\n if (sent.outcome !== \"ok\") throw new Error(`the limiter store ${limiter} failed (${sent.outcome}): ${sent.error}`);\n return replyOf(limiter, sent.value);\n },\n };\n};\n\n/** This function gives the decision of a reply of `LIMITER_SCRIPT`. A reply of another shape throws. */\nconst replyOf = (limiter: string, reply: unknown): Decision => {\n if (Array.isArray(reply) && Number(reply[0]) === 1 && reply.length === 2) return { allowed: true, remaining: Number(reply[1]) };\n if (Array.isArray(reply) && Number(reply[0]) === 0 && reply.length === 3 && (reply[2] === \"limit\" || reply[2] === \"cap\")) {\n const retryAfter = Number(reply[1]);\n if (Number.isFinite(retryAfter) && retryAfter > 0) return { allowed: false, retryAfter: { value: retryAfter, unit: \"ms\" }, cause: reply[2] };\n }\n throw new Error(`the limiter store ${limiter} gave a reply that is not a decision: ${JSON.stringify(reply)}`);\n};\n","import type { Machine } from \"@mark1russell7/algebra\";\nimport { clientKnob as knob, closeTimer, transition as t } from \"../client/machine.ts\";\n\n/** The states where the connection exists or tries to exist, thus where it can close. */\nconst LIVE = [\"connecting\", \"connect\", \"ready\", \"reconnecting\"] as const;\nconst ANY = [\"none\", \"connecting\", \"connect\", \"ready\", \"close\", \"reconnecting\", \"end\"] as const;\n\n/**\n * The Redis client as a `mark1russell7.machine/1` document. Its states are the states of ioredis.\n * The wrapper adds its own events: the join at each `ready`, and the misses of its watchdog. It also\n * adds a command that it sends again, and the bound of its close. A test records the transitions of the\n * client and replays them against this document.\n *\n * The state `ready` hides states that the client cannot see (D4). In a black hole, ioredis stays\n * `ready` while the kernel retransmits, until `driver_writeoff_time` or the watchdog ends that\n * hidden interval. A timer of this document bounds each hidden interval.\n */\nexport const REDIS_CLIENT_MACHINE: Machine = {\n format: \"mark1russell7.machine/1\",\n id: \"service.machine.redis-client\",\n title: \"The Redis client of the service, over ioredis 6\",\n layer: \"driver\",\n version: \"0.2.0\",\n sources: [{ ref: \"packages/service/src/redis (the client implements this document)\", rung: \"implemented\" }],\n states: {\n none: { initial: true, note: \"the client exists, and its connection does not\" },\n connecting: { note: \"a TCP connection attempt\" },\n connect: { note: \"the handshake: HELLO, then the ready check (INFO)\" },\n ready: { note: \"it can hide a black hole or a frozen server, until a timer of the client ends it\" },\n close: { note: \"the socket closed. Each command in flight settles as indeterminate\" },\n reconnecting: { note: \"the delay of the retry strategy, which doubles to its cap\" },\n end: { note: \"the retry window ended, or the service closed the client. The watchdog starts it again\" },\n },\n events: {\n \"status.connecting\": { kind: \"local\" },\n \"status.connect\": { kind: \"rx\" },\n \"status.ready\": { kind: \"rx\" },\n \"status.close\": { kind: \"rx\" },\n \"status.reconnecting\": { kind: \"timer\" },\n \"status.end\": { kind: \"local\" },\n join: { kind: \"rx\", note: \"the reply of CLIENT ID at ready (D1)\" },\n error: { kind: \"rx\" },\n \"connect.failed\": { kind: \"local\" },\n \"command.written\": { kind: \"tx\" },\n \"command.reissue\": { kind: \"local\" },\n data: { kind: \"rx\" },\n heartbeat: { kind: \"timer\" },\n \"watchdog.miss\": { kind: \"local\" },\n \"watchdog.reconnect\": { kind: \"local\" },\n \"watchdog.revive\": { kind: \"local\" },\n \"retry.window-ended\": { kind: \"local\" },\n \"close.request\": { kind: \"local\" },\n \"close.bound\": { kind: \"timer\" },\n \"close.destroy\": { kind: \"local\", note: \"the bound destroys a socket that a silent server does not close\" },\n },\n knobs: {\n connect_bound: knob(\"connectTimeout\"),\n driver_writeoff_time: knob(\"socketTimeout\"),\n heartbeat_interval: knob(\"watchdog.interval\"),\n close_bound: knob(\"close.bound\"),\n },\n timers: {\n connect_timeout: {\n duration: { knob: \"connect_bound\", unit: \"s\" },\n arms: [\"enter:connecting\"],\n cancels: [\"status.connect\", \"status.close\"],\n fires: \"status.close\",\n note: \"ioredis destroys the attempt\",\n },\n writeoff: {\n duration: { knob: \"driver_writeoff_time\", unit: \"s\" },\n arms: [\"command.written\"],\n cancels: [\"data\", \"status.close\"],\n fires: \"status.close\",\n note: \"ioredis destroys a socket that gives no data while a command waits: it ends the hidden interval of a black hole\",\n },\n heartbeat: {\n duration: { knob: \"heartbeat_interval\", unit: \"s\" },\n arms: [\"enter:ready\", \"heartbeat\"],\n cancels: [\"status.close\", \"status.end\"],\n fires: \"heartbeat\",\n note: \"a PING on the connection of the application\",\n },\n close: closeTimer(),\n },\n transitions: [\n t(\"connect\", [\"none\", \"reconnecting\", \"end\", \"close\"], \"status.connecting\", \"connecting\"),\n t(\"tcp\", \"connecting\", \"status.connect\", \"connect\"),\n t(\"ready\", \"connect\", \"status.ready\", \"ready\"),\n t(\"close\", [...LIVE], \"status.close\", \"close\"),\n t(\"retry\", \"close\", \"status.reconnecting\", \"reconnecting\"),\n t(\"end\", [...LIVE, \"close\"], \"status.end\", \"end\"),\n t(\"join\", \"ready\", \"join\", \"ready\"),\n t(\"miss\", \"ready\", \"watchdog.miss\", \"ready\"),\n t(\"watchdog-reconnect\", \"ready\", \"watchdog.reconnect\", \"ready\"),\n t(\"revive\", \"end\", \"watchdog.revive\", \"end\"),\n // the retry strategy answers in `close`, before ioredis moves to `reconnecting` or `end`\n t(\"window-ended\", \"close\", \"retry.window-ended\", \"close\"),\n ...ANY.map((state) => t(`error-${state}`, state, \"error\", state)),\n ...ANY.map((state) => t(`connect-failed-${state}`, state, \"connect.failed\", state)),\n ...ANY.map((state) => t(`reissue-${state}`, state, \"command.reissue\", state)),\n ...ANY.map((state) => t(`close-bound-${state}`, state, \"close.bound\", state, { timer: \"close\" })),\n ...ANY.map((state) => t(`close-destroy-${state}`, state, \"close.destroy\", state)),\n ],\n};\n"],"mappings":"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"}