@bumail/queue 0.3.0 → 0.4.0
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.
- package/README.md +4 -2
- package/dist/chunks/index-b4xvg3xn.js +23 -0
- package/dist/chunks/index-b4xvg3xn.js.map +10 -0
- package/dist/chunks/index-vg971acr.js.map +2 -2
- package/dist/contract/queue-store.d.ts +8 -2
- package/dist/contract/queue-store.d.ts.map +1 -1
- package/dist/dsn/build.d.ts +7 -3
- package/dist/dsn/build.d.ts.map +1 -1
- package/dist/errors.d.ts +3 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.js +45 -11
- package/dist/index.js.map +5 -5
- package/dist/masked.d.ts +2 -1
- package/dist/masked.d.ts.map +1 -1
- package/dist/postgres/connect.d.ts +6 -0
- package/dist/postgres/connect.d.ts.map +1 -1
- package/dist/postgres/index.js +47 -17
- package/dist/postgres/index.js.map +7 -6
- package/dist/postgres/isolation.d.ts +6 -0
- package/dist/postgres/isolation.d.ts.map +1 -0
- package/dist/postgres/schema.d.ts.map +1 -1
- package/dist/postgres/store.d.ts.map +1 -1
- package/dist/queue/deliver.d.ts +3 -1
- package/dist/queue/deliver.d.ts.map +1 -1
- package/dist/queue/notify.d.ts +3 -2
- package/dist/queue/notify.d.ts.map +1 -1
- package/dist/redis/connect.d.ts +2 -0
- package/dist/redis/connect.d.ts.map +1 -1
- package/dist/redis/index.js +14 -6
- package/dist/redis/index.js.map +4 -4
- package/dist/redis/store.d.ts.map +1 -1
- package/docs/guide.md +87 -11
- package/docs/roadmap.md +20 -1
- package/docs/troubleshooting.md +76 -5
- package/package.json +1 -1
- package/dist/chunks/index-y3n1w2az.js +0 -18
- package/dist/chunks/index-y3n1w2az.js.map +0 -10
package/docs/guide.md
CHANGED
|
@@ -98,6 +98,7 @@ recipient's outcome is recorded with the reply that decided it.
|
|
|
98
98
|
| `sendMail` refuses the route's options (`INVALID_OPTION`) | `deferred`, as `4.3.5` | tried again later, and an `error` event: the configuration is yours to fix |
|
|
99
99
|
| a recipient a store holds that `sendMail` would refuse | `failed`, as `5.1.3` | final, alone; a DSN |
|
|
100
100
|
| a sender a store holds that `sendMail` would refuse | `failed`, as `5.1.7` | final, every recipient, with no session; a DSN to that sender, which itself fails as `5.1.3` |
|
|
101
|
+
| a message the store lost ([below](#a-message-the-store-lost)) | `failed`, as `5.3.0` | final, every pending recipient, with no session; a DSN without the original, and a `MESSAGE_UNREADABLE` error |
|
|
101
102
|
|
|
102
103
|
An error is read by its `name` (`SmtpError`) and `code`, not by its
|
|
103
104
|
class: an app with a second copy of `@bumail/smtp` installed, or a
|
|
@@ -160,6 +161,40 @@ seconds (RFC 5321 §4.5.3.2's by default) and the whole session's (1800
|
|
|
160
161
|
seconds, DNS included). `createQueue` checks each one: above 0, and at
|
|
161
162
|
most 2147483 seconds, the longest a timer waits.
|
|
162
163
|
|
|
164
|
+
### A message the store lost
|
|
165
|
+
|
|
166
|
+
An item whose message the store no longer gives — a Redis key evicted
|
|
167
|
+
or deleted, a row removed by hand — cannot be sent. Rather than leave it
|
|
168
|
+
leased, claimed again at every lease and holding its `limits.maxItems`
|
|
169
|
+
place, the attempt fails every recipient still pending, at once:
|
|
170
|
+
|
|
171
|
+
- the failures are recorded, and the attempt counts; then
|
|
172
|
+
- the `error` event gets a `QueueError` of code `MESSAGE_UNREADABLE`,
|
|
173
|
+
`The message of … is unreadable: the store holds the item but not its
|
|
174
|
+
message, so every pending recipient failed`, with the item's `id`;
|
|
175
|
+
- each recipient's `failed` event follows, its `reply` `{ status:
|
|
176
|
+
'5.3.0', text: 'Message unreadable: the queue store holds the item but
|
|
177
|
+
not its message' }`, with no `code`, since no server answered;
|
|
178
|
+
- the failure DSN goes to the sender as usual — never about a message
|
|
179
|
+
from `<>` — with no third part, since there is nothing to return;
|
|
180
|
+
- the item leaves the queue. Recipients already delivered keep their
|
|
181
|
+
state, and are not told again.
|
|
182
|
+
|
|
183
|
+
In that order: the record, the `error`, the `failed` events, the `dsn`
|
|
184
|
+
event. Nothing is told before the record: when the lease was lost
|
|
185
|
+
meanwhile, the attempt reports `LEASE_LOST` only, and the worker that
|
|
186
|
+
holds the item next tells the failures; when `complete` throws, the
|
|
187
|
+
`error` event gets that error alone; when the item was cancelled
|
|
188
|
+
meanwhile, its lease still held, nothing is told at all — no session
|
|
189
|
+
ran, and nothing was recorded. An item that is gone along with its
|
|
190
|
+
message (cancelled, or finished by another worker) is left alone, as
|
|
191
|
+
before.
|
|
192
|
+
|
|
193
|
+
A store keeps an item from being claimed before its message can be read
|
|
194
|
+
— the message written first, or both in one step — so a message missing
|
|
195
|
+
at a claim is always damage, never an add still under way. Every store
|
|
196
|
+
here adds both in one step.
|
|
197
|
+
|
|
163
198
|
## Retries
|
|
164
199
|
|
|
165
200
|
```ts
|
|
@@ -196,7 +231,9 @@ final, and the item's `attempts` counts them.
|
|
|
196
231
|
The queue writes DSNs (RFC 3464) as a `multipart/report`
|
|
197
232
|
(`report-type=delivery-status`, RFC 6522) of three parts: text for a
|
|
198
233
|
person, the `message/delivery-status` fields a program reads, and the
|
|
199
|
-
original's header fields.
|
|
234
|
+
original's header fields. The third is left out, as RFC 6522 §3
|
|
235
|
+
allows, for [a message the store lost](#a-message-the-store-lost): the
|
|
236
|
+
text then says it could not be read.
|
|
200
237
|
|
|
201
238
|
```
|
|
202
239
|
Reporting-MTA: dns; mail.example.net
|
|
@@ -309,11 +346,19 @@ off(); // stops listening
|
|
|
309
346
|
| `deferred` | the same, and `nextAttemptAt` |
|
|
310
347
|
| `failed` | the same as `delivered` |
|
|
311
348
|
| `dsn` | `{ kind: 'delayed' \| 'failed', id, of, to, recipients }`: `id` is the DSN's own item, `of` the item it reports on |
|
|
312
|
-
| `error` | `{ error, id? }`: a store that failed (a lease renewal included), a DSN that could not be enqueued, a lease lost, a route `sendMail` refused (`INVALID_OPTION`) |
|
|
313
|
-
|
|
314
|
-
Events come once
|
|
315
|
-
|
|
316
|
-
|
|
349
|
+
| `error` | `{ error, id? }`: a store that failed (a lease renewal included), a DSN that could not be enqueued, a lease lost (`LEASE_LOST`), a message the store lost (`MESSAGE_UNREADABLE`), a route `sendMail` refused (`INVALID_OPTION`) |
|
|
350
|
+
|
|
351
|
+
Events about an outcome come once it is recorded, in this order: a
|
|
352
|
+
`MESSAGE_UNREADABLE` error when the store lost the message, each
|
|
353
|
+
recipient's `delivered`, `deferred` or `failed`, then the `dsn` events.
|
|
354
|
+
An item cancelled while it was delivered, its lease still held, has
|
|
355
|
+
what its sessions did told, with no DSN, though nothing is recorded;
|
|
356
|
+
for a message the store lost, no session ran, and nothing is told.
|
|
357
|
+
Other errors are told as they happen: a route `sendMail` refused during
|
|
358
|
+
its session, a lease lost or a store that failed in place of the
|
|
359
|
+
outcome, and a DSN that could not be enqueued, during the `dsn` step.
|
|
360
|
+
A listener that throws is ignored. `reply.code` is absent
|
|
361
|
+
when no server answered: a connection error, a timeout, the DNS.
|
|
317
362
|
|
|
318
363
|
## Admin
|
|
319
364
|
|
|
@@ -413,8 +458,23 @@ a store on the same database, and they share one queue.
|
|
|
413
458
|
- It is typed by its shape, `PostgresClient` (`unsafe`, `begin` and
|
|
414
459
|
`close`), so the declarations need nothing of `@types/bun`; a `Bun.SQL`
|
|
415
460
|
client fits it.
|
|
461
|
+
- **Writes run at `READ COMMITTED`**, whatever the client's sessions
|
|
462
|
+
default to: each transaction that writes — a claim, a renewal, an
|
|
463
|
+
outcome, an add, a reschedule, a cancel, a migration — starts with
|
|
464
|
+
`SET TRANSACTION ISOLATION LEVEL READ COMMITTED, READ WRITE`. A client
|
|
465
|
+
shared with code that sets `default_transaction_isolation` to
|
|
466
|
+
`repeatable read` or `serializable` (`new Bun.SQL({ url, connection: {
|
|
467
|
+
default_transaction_isolation: 'serializable' } })`) still serves the
|
|
468
|
+
queue: the claim's `FOR UPDATE SKIP LOCKED` and the `maxItems` lock
|
|
469
|
+
need that level, and under a stricter one concurrent writes would fail
|
|
470
|
+
with SQLSTATE 40001. Reads are one statement each, at the client's
|
|
471
|
+
level.
|
|
416
472
|
- **Nothing connects at `open`**: a wrong option is refused there, and a
|
|
417
|
-
database out of reach on the first call.
|
|
473
|
+
database out of reach on the first call. A URL is never repeated in an
|
|
474
|
+
error, and the password — the URL's, or a given `Bun.SQL`'s — is
|
|
475
|
+
masked should the database's reason name it: always where it stands
|
|
476
|
+
in a URL (`:…@`), and alone, elsewhere, when it is 4 characters or
|
|
477
|
+
more (masking a shorter one everywhere would garble the reason).
|
|
418
478
|
|
|
419
479
|
### The schema
|
|
420
480
|
|
|
@@ -431,7 +491,10 @@ They are the `bun:sqlite` store's tables, column for column. The prefix
|
|
|
431
491
|
is lowercase letters, digits and underscores, starting with a letter or
|
|
432
492
|
an underscore, 40 characters at most: it is written into the statements,
|
|
433
493
|
never bound. Give each queue its own prefix to keep several in one
|
|
434
|
-
database
|
|
494
|
+
database, and never share a prefix with another package's or
|
|
495
|
+
application's tables: the store cannot tell another `<prefix>schema` or
|
|
496
|
+
`<prefix>messages` from its own, and would read that version as the
|
|
497
|
+
queue's. A `bytea` holds 1 GB at most, far above `limits.maxMessageSize`
|
|
435
498
|
(25 MiB by default).
|
|
436
499
|
|
|
437
500
|
### Migrations
|
|
@@ -490,8 +553,9 @@ RETURNING …
|
|
|
490
553
|
|
|
491
554
|
### Pool size
|
|
492
555
|
|
|
493
|
-
|
|
494
|
-
|
|
556
|
+
A read holds a connection for one statement, and each write for one
|
|
557
|
+
short transaction: the isolation level, then its statement or two, then
|
|
558
|
+
the commit. A worker has at most
|
|
495
559
|
`concurrency` items (20) in flight, each mostly waiting on a remote SMTP
|
|
496
560
|
server, not the database: `Bun.SQL`'s default pool of 10 connections is
|
|
497
561
|
enough for one, and a call waits for a free connection rather than fail.
|
|
@@ -543,6 +607,9 @@ exactly once, against `redis:7`.
|
|
|
543
607
|
and a claim or an outcome is told on `error` (see Troubleshooting).
|
|
544
608
|
- One client is one connection, pipelined: every call of one store goes
|
|
545
609
|
through it, so there is no pool to size.
|
|
610
|
+
- A URL is never repeated in an error, and its password is masked should
|
|
611
|
+
Redis's or Bun's reason name it, as on PostgreSQL. A given client's
|
|
612
|
+
password is not known to the store, and not masked.
|
|
546
613
|
|
|
547
614
|
### The keys
|
|
548
615
|
|
|
@@ -626,7 +693,9 @@ instance between its steps.
|
|
|
626
693
|
`noeviction` it refuses the write (`OOM command not allowed`), and
|
|
627
694
|
`enqueue` rejects with that `RedisError`. An item whose hash is gone
|
|
628
695
|
all the same (evicted, or deleted by hand) is dropped by the next
|
|
629
|
-
claim that meets it, its message with it, and never leased.
|
|
696
|
+
claim that meets it, its message with it, and never leased. An item
|
|
697
|
+
whose message alone is gone fails its pending recipients at once
|
|
698
|
+
([a message the store lost](#a-message-the-store-lost)). Bound the queue with `limits.maxItems` and
|
|
630
699
|
`limits.maxMessageSize`, and size `maxmemory` for both.
|
|
631
700
|
- **An ACL user** needs the commands the store sends and the ones its
|
|
632
701
|
scripts call, on its prefix's keys (`~<keyPrefix>*`; here the
|
|
@@ -716,8 +785,15 @@ points at it, and `mxPort` its port.
|
|
|
716
785
|
A store implements `QueueStore`, throws `QueueError` with the contract's
|
|
717
786
|
codes, and keeps its promises:
|
|
718
787
|
|
|
788
|
+
- `add` never makes an item claimable before its message is readable:
|
|
789
|
+
write the message first, or both in one step.
|
|
719
790
|
- `claim` is atomic: two claimers, in any process, never get the same
|
|
720
791
|
item; an item whose lease expired is claimable again.
|
|
792
|
+
- `readMessage` gives `undefined` once the item is gone, and also when
|
|
793
|
+
the store still holds the item but has lost its message: the queue
|
|
794
|
+
then fails the item's pending recipients as `5.3.0`
|
|
795
|
+
([a message the store lost](#a-message-the-store-lost)), so never
|
|
796
|
+
throw for a missing message.
|
|
721
797
|
- `complete` applies the outcome, the schedule and the count, and lets go
|
|
722
798
|
of the lease, in one step and only for the lease's owner; a final
|
|
723
799
|
status is never changed; an item whose every recipient is final is
|
package/docs/roadmap.md
CHANGED
|
@@ -7,7 +7,7 @@ only number on it.
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
9
|
Nothing in progress: the MongoDB store, first under Next, is the next
|
|
10
|
-
one taken. The Redis store
|
|
10
|
+
one taken. The Redis store shipped in 0.3.0 (see Shipped).
|
|
11
11
|
|
|
12
12
|
## Next
|
|
13
13
|
|
|
@@ -65,6 +65,25 @@ Then:
|
|
|
65
65
|
|
|
66
66
|
### Unreleased — merged, not yet published
|
|
67
67
|
|
|
68
|
+
- **The PostgreSQL store's writes run at `READ COMMITTED`** — whatever
|
|
69
|
+
the client's sessions default to, so a client shared with code that
|
|
70
|
+
defaults to `repeatable read` or `serializable` serves the queue:
|
|
71
|
+
concurrent claims, outcomes, adds with `maxItems` and migrations no
|
|
72
|
+
longer fail with SQLSTATE 40001, and `maxItems` holds under them.
|
|
73
|
+
- **A message the store lost no longer loops** — an item whose message
|
|
74
|
+
is gone (a Redis key evicted or deleted, a row removed by hand) fails
|
|
75
|
+
its pending recipients at once as `5.3.0`, `Message unreadable`, says
|
|
76
|
+
so on the `error` event (`MESSAGE_UNREADABLE`), sends its failure DSN
|
|
77
|
+
without the original and leaves the queue, rather than be claimed
|
|
78
|
+
again at every lease, forever, holding its `maxItems` place.
|
|
79
|
+
- **A set-up error never shows the password** — `The PostgreSQL queue
|
|
80
|
+
cannot be set up` and `The Redis queue cannot be set up` mask it
|
|
81
|
+
should the database's reason repeat it; and a password under 4
|
|
82
|
+
characters is masked only where a URL holds it, so the rest of the
|
|
83
|
+
reason stays readable.
|
|
84
|
+
|
|
85
|
+
### 0.3.0
|
|
86
|
+
|
|
68
87
|
- **A Redis store**, `@bumail/queue/redis`, on Bun's own `Bun.redis`,
|
|
69
88
|
for instances of a server on several machines sharing one queue:
|
|
70
89
|
`RedisQueueStore.open({ client })` takes a `Bun.RedisClient`, or
|
package/docs/troubleshooting.md
CHANGED
|
@@ -36,10 +36,12 @@ parts shown as … vary.
|
|
|
36
36
|
- [`QueueError: The lease on … was lost while it was delivered`](#queueerror-the-lease-on--was-lost-while-it-was-delivered)
|
|
37
37
|
- [`QueueError: The lease on … expired before its outcome was recorded, and the item is gone: lost to another worker that finished it, the message then sent twice, or cancelled`](#queueerror-the-lease-on--expired-before-its-outcome-was-recorded-and-the-item-is-gone-lost-to-another-worker-that-finished-it-the-message-then-sent-twice-or-cancelled)
|
|
38
38
|
- [`QueueError: The lease on … was lost while it was delivered, and the item is gone: finished by the worker that took it, the message then sent twice, or cancelled`](#queueerror-the-lease-on--was-lost-while-it-was-delivered-and-the-item-is-gone-finished-by-the-worker-that-took-it-the-message-then-sent-twice-or-cancelled)
|
|
39
|
+
- [`QueueError: The message of … is unreadable: the store holds the item but not its message, so every pending recipient failed`](#queueerror-the-message-of--is-unreadable-the-store-holds-the-item-but-not-its-message-so-every-pending-recipient-failed)
|
|
39
40
|
- [`SmtpError: sendMail(): …` (`INVALID_OPTION`), with recipients deferred as `4.3.5`](#smtperror-sendmail--invalid_option-with-recipients-deferred-as-435)
|
|
40
41
|
- [`SQLiteError: database is locked`, or another store error, during a delivery](#sqliteerror-database-is-locked-or-another-store-error-during-a-delivery)
|
|
41
42
|
- [`PostgresError: …`, or a connection error, during a delivery](#postgreserror--or-a-connection-error-during-a-delivery)
|
|
42
43
|
- [`RedisError: …` during a delivery: `Connection closed`, `OOM command not allowed …`, `READONLY …`](#rediserror--during-a-delivery-connection-closed-oom-command-not-allowed--readonly-)
|
|
44
|
+
- [`PostgresError: could not serialize access due to concurrent update` (SQLSTATE `40001`)](#postgreserror-could-not-serialize-access-due-to-concurrent-update-sqlstate-40001)
|
|
43
45
|
|
|
44
46
|
**The `bun:sqlite` store** (`@bumail/queue/sqlite`)
|
|
45
47
|
|
|
@@ -87,6 +89,7 @@ parts shown as … vary.
|
|
|
87
89
|
- [Every delivery fails at once with `5.7.1`, or with a reply naming your IP or EHLO name](#every-delivery-fails-at-once-with-571-or-with-a-reply-naming-your-ip-or-ehlo-name)
|
|
88
90
|
- [A recipient fails with `5.1.3`, `The address is not one SMTP can carry`](#a-recipient-fails-with-513-the-address-is-not-one-smtp-can-carry)
|
|
89
91
|
- [Every recipient fails with `5.1.7`, `The sender's address is not one SMTP can carry`](#every-recipient-fails-with-517-the-senders-address-is-not-one-smtp-can-carry)
|
|
92
|
+
- [Every pending recipient fails with `5.3.0`, `Message unreadable: the queue store holds the item but not its message`](#every-pending-recipient-fails-with-530-message-unreadable-the-queue-store-holds-the-item-but-not-its-message)
|
|
90
93
|
- [`retryNow` returns `false` for an item that is in the queue](#retrynow-returns-false-for-an-item-that-is-in-the-queue)
|
|
91
94
|
|
|
92
95
|
## Enqueuing
|
|
@@ -323,6 +326,29 @@ the store keeps no record of which happened, so the message names both.
|
|
|
323
326
|
**Fix:** a longer `leaseMs`, find what stalled the process, and keep the
|
|
324
327
|
machines on NTP.
|
|
325
328
|
|
|
329
|
+
### `QueueError: The message of … is unreadable: the store holds the item but not its message, so every pending recipient failed`
|
|
330
|
+
|
|
331
|
+
**Code:** `MESSAGE_UNREADABLE`, with the item's `id`.
|
|
332
|
+
**When:** a worker claimed an item, the store gave no message for it
|
|
333
|
+
while it still held the item, and the failures were then recorded: it
|
|
334
|
+
comes after the record, before the `failed` events. With the lease lost
|
|
335
|
+
before the record, only `LEASE_LOST` is told, and the next worker tells
|
|
336
|
+
this; with the item cancelled meanwhile, nothing is.
|
|
337
|
+
**Why:** the message is gone and the item is not: on Redis, its
|
|
338
|
+
`<prefix>message:<id>` key evicted under a `maxmemory-policy` other than
|
|
339
|
+
`noeviction`, or deleted; on any store, a message row or key removed by
|
|
340
|
+
hand, or a store of your own that loses it. Nothing can be sent, so
|
|
341
|
+
every recipient still pending fails as `5.3.0` (see
|
|
342
|
+
[below](#every-pending-recipient-fails-with-530-message-unreadable-the-queue-store-holds-the-item-but-not-its-message)),
|
|
343
|
+
the failure DSN goes to the sender without the original, and the item
|
|
344
|
+
leaves the queue. Before this, such an item stayed leased and was
|
|
345
|
+
claimed again at every lease, forever, with no event.
|
|
346
|
+
**Fix:** find what removed the message. On Redis, set
|
|
347
|
+
`maxmemory-policy noeviction` and size `maxmemory` for the queue (the
|
|
348
|
+
guide's [Redis](guide.md#redis)); never delete a queue's keys or rows by
|
|
349
|
+
hand, `cancel(id)` drops an item with its message. The sender has the
|
|
350
|
+
DSN and may send again.
|
|
351
|
+
|
|
326
352
|
### `SmtpError: sendMail(): …` (`INVALID_OPTION`), with recipients deferred as `4.3.5`
|
|
327
353
|
|
|
328
354
|
**When:** `sendMail` refused the options of a session — the route, the
|
|
@@ -387,6 +413,26 @@ your provider's endpoint or Sentinel's, which follows a failover). For
|
|
|
387
413
|
`maxmemory-policy noeviction` (the guide's [Redis](guide.md#redis)), or
|
|
388
414
|
Redis drops queue items to make room instead of refusing.
|
|
389
415
|
|
|
416
|
+
### `PostgresError: could not serialize access due to concurrent update` (SQLSTATE `40001`)
|
|
417
|
+
|
|
418
|
+
**When:** on `@bumail/queue/postgres` 0.3.0 or earlier — a claim, an
|
|
419
|
+
outcome, an `enqueue` with `limits.maxItems` or a migration rejecting, or
|
|
420
|
+
told on `error`, with `could not serialize access due to concurrent
|
|
421
|
+
update` or `… due to read/write dependencies among transactions`, while
|
|
422
|
+
several workers run — or `limits.maxItems` letting more items in than
|
|
423
|
+
its limit.
|
|
424
|
+
**Why:** the client's sessions default to `repeatable read` or
|
|
425
|
+
`serializable` (`connection: { default_transaction_isolation }`, or the
|
|
426
|
+
role's or database's own default). The claim's `FOR UPDATE SKIP LOCKED`
|
|
427
|
+
and the `maxItems` advisory lock need `READ COMMITTED`: under a stricter
|
|
428
|
+
level, a row another worker changed since the transaction's snapshot
|
|
429
|
+
fails, and the count taken after the lock reads a snapshot from before
|
|
430
|
+
it.
|
|
431
|
+
**Fix:** upgrade: every transaction the store writes in now starts with
|
|
432
|
+
`SET TRANSACTION ISOLATION LEVEL READ COMMITTED`, whatever the client's
|
|
433
|
+
default. The error from a transaction of your own on the same client is
|
|
434
|
+
yours to retry.
|
|
435
|
+
|
|
390
436
|
## The `bun:sqlite` store
|
|
391
437
|
|
|
392
438
|
### `QueueError: A SQLite queue store needs a directory`
|
|
@@ -447,7 +493,8 @@ PostgresQueueStore.open({ sql: new Bun.SQL({ url, max: 10 }) });
|
|
|
447
493
|
**Why:** the prefix is written into every statement, never bound as a
|
|
448
494
|
value, so anything that is not a plain name is refused — and 40
|
|
449
495
|
characters keep the longest name built on it within PostgreSQL's 63.
|
|
450
|
-
**Fix:** `tablePrefix: 'mail_queue_'
|
|
496
|
+
**Fix:** `tablePrefix: 'mail_queue_'`, a prefix of the queue's own: never
|
|
497
|
+
one another package's or application's tables use. To put the tables in another
|
|
451
498
|
schema, set the connection's `search_path` rather than a dotted prefix.
|
|
452
499
|
|
|
453
500
|
### `QueueError: The URL in sql cannot be opened: …`
|
|
@@ -457,7 +504,10 @@ schema, set the connection's `search_path` rather than a dotted prefix.
|
|
|
457
504
|
connecting, such as a `sslmode` it does not know. The rest is Bun's
|
|
458
505
|
reason (`The argument 'sslmode' must be one of: disable, allow, prefer,
|
|
459
506
|
require, verify-ca, verify-full. Received '…'`); the URL is never
|
|
460
|
-
repeated, and the password is masked should the reason name it
|
|
507
|
+
repeated, and the password is masked should the reason name it: always
|
|
508
|
+
where a URL holds it (`:…@`), and alone, elsewhere, when it is 4
|
|
509
|
+
characters or more — masking `x` everywhere would turn "execute" into
|
|
510
|
+
"e…ecute".
|
|
461
511
|
**Fix:** correct the parameter: `?sslmode=require`, or `verify-full` to
|
|
462
512
|
check the server's certificate.
|
|
463
513
|
|
|
@@ -467,7 +517,10 @@ check the server's certificate.
|
|
|
467
517
|
**When:** the first call on a store, or `migrate()`, when the tables
|
|
468
518
|
cannot be made or brought up to date. The rest of the message is the
|
|
469
519
|
database's: `Failed to connect`, `password authentication failed for
|
|
470
|
-
user "…"`, `permission denied for schema public`.
|
|
520
|
+
user "…"`, `permission denied for schema public`. The password — the
|
|
521
|
+
URL's, or a given `Bun.SQL`'s — is masked should the reason name it, as
|
|
522
|
+
for `The URL in sql cannot be opened`: a `…` where you expect a role or
|
|
523
|
+
a database name is one that equals the password.
|
|
471
524
|
**Why:** the database is out of reach, the credentials are wrong, or the
|
|
472
525
|
tables are missing or behind and the role may not create them: a role
|
|
473
526
|
without `CREATE` can use tables that are current, never make them.
|
|
@@ -516,7 +569,8 @@ database its own prefix.
|
|
|
516
569
|
**When:** `RedisQueueStore.open` with a URL `Bun.RedisClient` refuses
|
|
517
570
|
before connecting, such as a database that is not a number (`Invalid
|
|
518
571
|
database number in Redis URL: "…"`). The URL is never repeated, and the
|
|
519
|
-
password is masked should the reason name it
|
|
572
|
+
password is masked should the reason name it, as on PostgreSQL: always
|
|
573
|
+
in a URL, and alone from 4 characters.
|
|
520
574
|
**Fix:** correct the URL: the database is the path, `/0` to `/15` on a
|
|
521
575
|
default Redis.
|
|
522
576
|
|
|
@@ -528,7 +582,8 @@ its keys (`<prefix>schema`), writing it on a new queue. The rest of the
|
|
|
528
582
|
message is Redis's or Bun's: `Max reconnection attempts reached` (out of
|
|
529
583
|
reach), `WRONGPASS invalid username-password pair or user is disabled.`,
|
|
530
584
|
`ERR DB index is out of range`, `NOPERM …` (an ACL user without
|
|
531
|
-
`EVALSHA`, `EVAL` or the keys).
|
|
585
|
+
`EVALSHA`, `EVAL` or the keys). The URL's password is masked should the
|
|
586
|
+
reason repeat it; a given client's is not known to the store.
|
|
532
587
|
**Why:** the server is out of reach, the credentials or the database are
|
|
533
588
|
wrong, or the user may not run scripts. With `Bun.RedisClient`'s
|
|
534
589
|
defaults an unreachable server takes about half a minute of reconnecting
|
|
@@ -697,6 +752,22 @@ other.
|
|
|
697
752
|
`@bumail/smtp/client`'s `isMailbox` (or let it be `''`) before a store of
|
|
698
753
|
your own keeps it.
|
|
699
754
|
|
|
755
|
+
### Every pending recipient fails with `5.3.0`, `Message unreadable: the queue store holds the item but not its message`
|
|
756
|
+
|
|
757
|
+
**When:** the store held the item but gave no message for it, and the
|
|
758
|
+
failures were recorded: the `error` event says
|
|
759
|
+
[`The message of … is unreadable`](#queueerror-the-message-of--is-unreadable-the-store-holds-the-item-but-not-its-message-so-every-pending-recipient-failed)
|
|
760
|
+
for the same item, just before these `failed` events. An item cancelled
|
|
761
|
+
before the record tells no `5.3.0` failure; with the lease lost, this
|
|
762
|
+
attempt tells none and the next worker tells them.
|
|
763
|
+
**Why:** no session can send a message that cannot be read, and an
|
|
764
|
+
attempt left without an outcome would be claimed again at every lease,
|
|
765
|
+
forever, keeping its `maxItems` place. Recipients already delivered keep
|
|
766
|
+
their state; the others fail in one attempt, with one failure DSN —
|
|
767
|
+
none for a message from `<>` — that returns nothing of the original.
|
|
768
|
+
**Fix:** as for that error: find what removed the message, and on Redis
|
|
769
|
+
keep `maxmemory-policy noeviction`.
|
|
770
|
+
|
|
700
771
|
### `retryNow` returns `false` for an item that is in the queue
|
|
701
772
|
|
|
702
773
|
**Why:** a worker is delivering it: its lease is held. That attempt's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bumail/queue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The outbound mail queue of a mail server: every recipient's state, retries with back-off, delivery status notifications (RFC 3464), leases for several workers, and a memory, a bun:sqlite, a PostgreSQL (Bun.sql) and a Redis (Bun.redis) store, delivering through @bumail/smtp/client",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// src/masked.ts
|
|
2
|
-
function masked(text, raw) {
|
|
3
|
-
let decoded = raw;
|
|
4
|
-
try {
|
|
5
|
-
decoded = decodeURIComponent(raw);
|
|
6
|
-
} catch {}
|
|
7
|
-
let out = text;
|
|
8
|
-
for (const password of [raw, decoded]) {
|
|
9
|
-
if (password !== "")
|
|
10
|
-
out = out.replaceAll(password, "…");
|
|
11
|
-
}
|
|
12
|
-
return out;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export { masked };
|
|
16
|
-
|
|
17
|
-
//# debugId=64BFD310901EABF064756E2164756E21
|
|
18
|
-
//# sourceMappingURL=index-y3n1w2az.js.map
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/masked.ts"],
|
|
4
|
-
"sourcesContent": [
|
|
5
|
-
"/**\n * `text` with a URL's password masked, as written in the URL and decoded,\n * if it decodes: what a store opening a URL repeats of the client's\n * reason, which may name it.\n */\nexport function masked(text: string, raw: string): string {\n\tlet decoded = raw;\n\ttry {\n\t\tdecoded = decodeURIComponent(raw);\n\t} catch {\n\t\t// Not percent-encoding a client reads either: the raw form is what it would repeat.\n\t}\n\tlet out = text;\n\tfor (const password of [raw, decoded]) {\n\t\tif (password !== '') out = out.replaceAll(password, '…');\n\t}\n\treturn out;\n}\n"
|
|
6
|
-
],
|
|
7
|
-
"mappings": ";AAKO,SAAS,MAAM,CAAC,MAAc,KAAqB;AAAA,EACzD,IAAI,UAAU;AAAA,EACd,IAAI;AAAA,IACH,UAAU,mBAAmB,GAAG;AAAA,IAC/B,MAAM;AAAA,EAGR,IAAI,MAAM;AAAA,EACV,WAAW,YAAY,CAAC,KAAK,OAAO,GAAG;AAAA,IACtC,IAAI,aAAa;AAAA,MAAI,MAAM,IAAI,WAAW,UAAU,GAAG;AAAA,EACxD;AAAA,EACA,OAAO;AAAA;",
|
|
8
|
-
"debugId": "64BFD310901EABF064756E2164756E21",
|
|
9
|
-
"names": []
|
|
10
|
-
}
|