Cluster: A way to remove processed requests from storage
メンテナーはふだん 1 日以内に返信
評価
この issue はまだ評価されていません。
説明
What is the problem this feature would solve?
Base: effect 4.1.1 (effect/cluster, SqlMessageStorage). main has the same code.
I looked through MessageStorage on main, the issues and the PRs, and found nothing for this, but I may have missed it.
A persisted request is deduplicated by its row in cluster_messages, and answered from cluster_replies. Nothing
ever deletes these rows once the request is processed. Two operations remove them, and neither fits a request that
is finished:
clearReplies(requestId)deletes the reply, then setsprocessed = falseon the request. The request is
delivered again and the handler runs again.clearAddress(address)deletes every message and reply of the entity. That works only for an entity that is
gone.
As far as I can tell, the tables of an entity that lives a long time grow with every request it handles, with no
limit. The workflow engine's rows grow the same way, apart from clearAddress on the clock entity. The application
often knows when a request can no longer be retried. For example, the sender's work has finished, so nobody will
send that primary key again. But I found no API to tell storage.
Repro (SingleRunner on @effect/sql-sqlite-bun, one entity whose handler counts its runs):
let runs = 0
const Hit = Rpc.make("Hit", { payload: { id: Schema.String }, success: Schema.Number, primaryKey: ({ id }) => id })
const Counter = Entity.make("Counter", [Hit])
.annotateRpcs(ClusterSchema.Persisted, true)
.annotateRpcs(ClusterSchema.WithTransaction, true)
const CounterLive = Counter.toLayer({ Hit: () => Effect.sync(() => ++runs) })
const client = (yield* Counter.client)("one")
yield* client.Hit({ id: "a" })
yield* client.Hit({ id: "a" }) // deduplicated
for (let i = 0; i < 100; i++) yield* client.Hit({ id: `b${i}` })
yield* storage.clearReplies(requestIdOf("a")) // nobody sends "a" again
yield* sharding.pollStorage
yield* sql`DELETE FROM cluster_replies WHERE request_id = ${requestIdOf("b0")}`
yield* sql`DELETE FROM cluster_messages WHERE request_id = ${requestIdOf("b0")}`
yield* client.Hit({ id: "b0" })
| Step | Handler runs | Rows in cluster_messages / cluster_replies |
|---|---|---|
| "a" sent twice | 1 | 1 / 1 |
| 100 more requests, then a wait | 101 | 101 / 101 |
clearReplies on "a", then a poll |
102: "a" ran again | 101 / 101 |
| rows of "b0" deleted by hand | 102 | 100 / 100 |
| "b0" sent again | 103: a new request | 101 / 101 |
Is there an intended way to remove processed requests? For example, are long-lived entities not the intended use,
with short-lived entities cleared by clearAddress instead? If so, a note in the docs would help, and the rest of
this issue can be ignored.
What is the feature you are proposing to solve the problem?
A way to forget processed requests, deleting the request and its replies together without
delivering it again. After that, a request sent with the same primary key would be new.
One possible shape (a sketch, open to a better one; I don't know the constraints you see):
// MessageStorage
readonly forgetRequests: (
requestIds: ReadonlyArray<Snowflake.Snowflake>
) => Effect.Effect<void, PersistenceError>
SqlMessageStoragedeletes, in one transaction, every reply of each request, its chunks included, and every message of it: the request and itsAckChunkandInterruptenvelopes. It works byrequest_id, so amessage_idstored as its hash makes no difference. It does this only where the request is processed. An unprocessed request is left alone, so a message is never dropped before it is handled. Processed rows are no longer read by the mailbox (unprocessedMessages) or byresetShards, so the runner that owns the shard does not miss them.- It runs inside the caller's transaction when there is one, like the other writes. So a
WithTransaction
handler can forget other requests in the same commit as its own writes. MemoryMessageStoragedoes the same.- Once a fence like the one in #10058 exists,
forgetRequestsgoes through it, likeclearRepliesand
clearAddress.
The contract the caller would keep. A forgotten request leaves nothing for a waiting caller to read. I read the
first point below from the source, but could not reproduce it on SingleRunner, where a duplicate gets its reply
from memory:
- A caller that waits for the reply from storage never gets it. This is the read loop in
Runners.ts, used when
the request was sent from another runner, when its handler was dropped (for example, the shard moved), or for a
duplicate. A runner knows only about the reply handlers registered on itself, not about callers on other runners,
so storage cannot check this. The caller offorgetRequestsmust know that nobody waits for the request any more. - The same primary key sent again is a new request, and the handler runs again.
What it would not cover:
- A handler's own request. A
WithTransactionhandler cannot forget its own request in its own commit. The
reply is saved after the handler returns, so deleting the request inside the handler leaves an orphan row in
cluster_replies. Forgetting it inafterCommitworks, but a crash between the commit and the forget leaves
the rows. An option on the request ("forget once replied") would close this. I have no design for it. - Workflows. Forgetting a workflow's or an activity's request would make
ClusterWorkflowEnginesee it as never
run, and run it again. A safe cleanup there would be an engine operation that purges a finished execution, not
this one.
What alternatives have you considered?
- Short-lived entities cleared with
clearAddress. This works when an entity's whole history can go at once.
It does not fit an entity that lives a long time and handles many requests, where only the finished ones should
go. clearReplies. It deletes the reply but delivers the request again, so the handler runs again.- A time-based option (keep processed requests for a set duration). It needs no call from the application, but
it is safe only where the application knows that no retry and no waiting caller comes after that time. So to me
the explicit call seems the more general of the two, but you may see it differently. - The application's own
DELETEstatements on Cluster's tables, as in the repro. That works, but it ties the
application to Cluster's schema, which can change in any release, and I would rather use a supported way.
- 主要言語
- TypeScript
- スター
- 16.7k
- フォーク
- 808
- 平均マージ
- 11時間 3分
- マージ済み PR(30日)
- 840
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
Effect-TS/effect のほかの issue
-
PgClient: withTransaction dies instead of failing with SqlError when the server ends the session対応中かも @effect-bot が今日担当しました。 オープン
Effect-TS/effect#10290 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
shorthand for mapping over tagged union schema対応中かも @effect-bot が今日担当しました。 オープンenhancement
Effect-TS/effect#10286 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
難易度 5/5 1週間以上 初心者へのやさしさ 35/100
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 47/100
メンテナーはふだん 1 日以内に返信
-
`fromJsonSchemaDocument`: import open `patternProperties`対応中かも @effect-bot が 5 日前に担当しました。 オープン
Effect-TS/effect#10048 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
Effect-TS/effect の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
kunchenguid/gh-axi#247 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
lidge-jun/opencodex#8101 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
effort:S P3 refactor
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
nightscout/nocturne#3243 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
JuliusBrussee/caveman#1449 ·
メンテナーはふだん 1 日以内に返信