[{"data":1,"prerenderedAt":478},["ShallowReactive",2],{"docs:\u002Fguide\u002Foverview":3},{"page":4,"untranslated":477,"translated":477},{"id":5,"title":6,"body":7,"description":468,"extension":469,"meta":470,"navigation":181,"path":471,"publishedAt":472,"seo":473,"stem":474,"updatedAt":475,"__hash__":476},"docs_ja\u002Fguide\u002Foverview.md","概要",{"type":8,"value":9,"toc":457},"minimark",[10,13,27,31,34,39,45,55,58,64,77,80,86,98,101,104,123,126,129,383,406,409,431,434,447,453],[11,12,6],"h1",{"id":6},[14,15,16,17,20,21,23,24,26],"p",{},"Tsumugiは、Cloudflare Workers向けに設計されたジョブマネジメントシステムです。",[18,19],"br",{},"\n利用者は、ジョブの処理をperformerとして定義し、enqueueでジョブを投入します。",[18,22],{},"\nジョブの配送, 実行, 状態の記録はTsumugiが担います。",[18,25],{},"\n管理用のダッシュボードとREST APIも提供します。",[28,29,30],"h2",{"id":30},"何を解決するの",[14,32,33],{},"Cloudflare Queuesをジョブキューとして直接使うといくつかの問題が生じます。",[35,36,38],"h3",{"id":37},"ジョブの状態を管理しづらく成功したジョブが残らない","ジョブの状態を管理しづらく、成功したジョブが残らない",[14,40,41,42,44],{},"Queuesは、投入したメッセージの状態を問い合わせるAPIを提供していません。",[18,43],{},"\n成功したメッセージも保持されず、過去の実行を後から確認する手段が用意されていません。",[14,46,47,48,50,54],{},"TsumugiのダッシュボードとREST APIは、実行中のジョブと終了したジョブの両方について、一覧表示, 検索, 集計を提供します。",[18,49],{},[51,52,53],"code",{},"TSUMUGI_METRICS","のbindingを設定すると、失敗率と実行時間がメトリクスとして記録されます。",[35,56,57],{"id":57},"排他制御と重複排除を実装しづらい",[14,59,60,61,63],{},"Queuesは、メッセージ間の依存関係や関連性を管理しません。",[18,62],{},"\nそのため、同一顧客に対する同時実行の防止や、重複投入の排除は、利用者が個別に実装する必要があります。",[14,65,66,67,70,71,73,76],{},"Tsumugiでは、",[51,68,69],{},"concurrencyKey","を指定したジョブは、同じキーのものから1件ずつ順に実行されます。",[18,72],{},[51,74,75],{},"uniqueKey","を指定すると、一定期間内に同じキーで投入されたジョブは重複とみなされ、新しいジョブを作らずに既存のジョブIDを戻り値とします。",[35,78,79],{"id":79},"ジョブの種類が増えるほど分岐が増える",[14,81,82,83,85],{},"Queuesでは、1つのキューに対してconsumerを1つしか設定できません。",[18,84],{},"\nそのため、ジョブの種類が増えると、キューの数を増やすか、consumer内での分岐処理が複雑になっていきます。",[14,87,88,89,91,92,97],{},"Tsumugiは、performerのexport名をbinding名としてジョブの種類を区別するため、consumerに分岐を書く必要がありません。",[18,90],{},"\nbinding名とpayloadの型の関係は、",[93,94,96],"a",{"href":95},"\u002Fguide\u002Fperformer#binding%E5%90%8D","binding名","で説明しています。",[35,99,100],{"id":100},"まれにメッセージが失われる",[14,102,103],{},"Queuesは、処理されずに失われたメッセージを検出する手段を提供していません。",[14,105,106,107,109,112,113,115,118,119,122],{},"Tsumugiは、実行を開始したジョブから結果が報告されない場合、そのジョブを無応答と判定します。",[18,108],{},[51,110,111],{},"at-least-once","セマンティクスの場合、無応答のジョブは再投入されます。",[18,114],{},[51,116,117],{},"at-most-once","セマンティクスの場合、無応答のジョブは",[51,120,121],{},"STALLED","状態に遷移し、手動でリトライされるまで実行されません。",[28,124,125],{"id":125},"利用者が記述するコード",[14,127,128],{},"記述するのはジョブの処理の定義と投入のたった2箇所だけです。",[130,131,136],"pre",{"className":132,"code":133,"language":134,"meta":135,"style":135},"language-ts shiki shiki-themes github-light github-dark","import { defineTsumugi } from 'tsumugi';\nimport { Performer } from 'tsumugi\u002Fperformer';\n\n\u002F\u002F 1. ジョブの処理をperformerとして定義してexportする\nexport class SendMail extends Performer\u003C{ to: string }, void, {}, Env> {\n  async perform(payload: { to: string }): Promise\u003Cvoid> {\n    await this.env.MAILER.send(payload.to);\n  }\n}\n\nconst tsumugi = defineTsumugi({ performers: { SendMail } });\n\n\u002F\u002F 2. 投入する。binding名はexportした名前\nconst id = await tsumugi.enqueue(env, { binding: 'SendMail', payload: { to: 'a@example.com' } });\n","ts","",[51,137,138,161,176,183,190,238,279,303,309,315,320,338,343,349],{"__ignoreMap":135},[139,140,143,147,151,154,158],"span",{"class":141,"line":142},"line",1,[139,144,146],{"class":145},"szBVR","import",[139,148,150],{"class":149},"sVt8B"," { defineTsumugi } ",[139,152,153],{"class":145},"from",[139,155,157],{"class":156},"sZZnC"," 'tsumugi'",[139,159,160],{"class":149},";\n",[139,162,164,166,169,171,174],{"class":141,"line":163},2,[139,165,146],{"class":145},[139,167,168],{"class":149}," { Performer } ",[139,170,153],{"class":145},[139,172,173],{"class":156}," 'tsumugi\u002Fperformer'",[139,175,160],{"class":149},[139,177,179],{"class":141,"line":178},3,[139,180,182],{"emptyLinePlaceholder":181},true,"\n",[139,184,186],{"class":141,"line":185},4,[139,187,189],{"class":188},"sJ8bj","\u002F\u002F 1. ジョブの処理をperformerとして定義してexportする\n",[139,191,193,196,199,203,206,209,212,216,219,223,226,229,232,235],{"class":141,"line":192},5,[139,194,195],{"class":145},"export",[139,197,198],{"class":145}," class",[139,200,202],{"class":201},"sScJk"," SendMail",[139,204,205],{"class":145}," extends",[139,207,208],{"class":201}," Performer",[139,210,211],{"class":149},"\u003C{ ",[139,213,215],{"class":214},"s4XuR","to",[139,217,218],{"class":145},":",[139,220,222],{"class":221},"sj4cs"," string",[139,224,225],{"class":149}," }, ",[139,227,228],{"class":221},"void",[139,230,231],{"class":149},", {}, ",[139,233,234],{"class":201},"Env",[139,236,237],{"class":149},"> {\n",[139,239,241,244,247,250,253,255,258,260,262,264,267,269,272,275,277],{"class":141,"line":240},6,[139,242,243],{"class":145},"  async",[139,245,246],{"class":201}," perform",[139,248,249],{"class":149},"(",[139,251,252],{"class":214},"payload",[139,254,218],{"class":145},[139,256,257],{"class":149}," { ",[139,259,215],{"class":214},[139,261,218],{"class":145},[139,263,222],{"class":221},[139,265,266],{"class":149}," })",[139,268,218],{"class":145},[139,270,271],{"class":201}," Promise",[139,273,274],{"class":149},"\u003C",[139,276,228],{"class":221},[139,278,237],{"class":149},[139,280,282,285,288,291,294,297,300],{"class":141,"line":281},7,[139,283,284],{"class":145},"    await",[139,286,287],{"class":221}," this",[139,289,290],{"class":149},".env.",[139,292,293],{"class":221},"MAILER",[139,295,296],{"class":149},".",[139,298,299],{"class":201},"send",[139,301,302],{"class":149},"(payload.to);\n",[139,304,306],{"class":141,"line":305},8,[139,307,308],{"class":149},"  }\n",[139,310,312],{"class":141,"line":311},9,[139,313,314],{"class":149},"}\n",[139,316,318],{"class":141,"line":317},10,[139,319,182],{"emptyLinePlaceholder":181},[139,321,323,326,329,332,335],{"class":141,"line":322},11,[139,324,325],{"class":145},"const",[139,327,328],{"class":221}," tsumugi",[139,330,331],{"class":145}," =",[139,333,334],{"class":201}," defineTsumugi",[139,336,337],{"class":149},"({ performers: { SendMail } });\n",[139,339,341],{"class":141,"line":340},12,[139,342,182],{"emptyLinePlaceholder":181},[139,344,346],{"class":141,"line":345},13,[139,347,348],{"class":188},"\u002F\u002F 2. 投入する。binding名はexportした名前\n",[139,350,352,354,357,359,362,365,368,371,374,377,380],{"class":141,"line":351},14,[139,353,325],{"class":145},[139,355,356],{"class":221}," id",[139,358,331],{"class":145},[139,360,361],{"class":145}," await",[139,363,364],{"class":149}," tsumugi.",[139,366,367],{"class":201},"enqueue",[139,369,370],{"class":149},"(env, { binding: ",[139,372,373],{"class":156},"'SendMail'",[139,375,376],{"class":149},", payload: { to: ",[139,378,379],{"class":156},"'a@example.com'",[139,381,382],{"class":149}," } });\n",[14,384,385,386,388,389,393,394,393,398,393,402,97],{},"キューの構成, consumerの分岐, 状態の記録を書く必要はありません。",[18,387],{},"\n各機能は、",[93,390,392],{"href":391},"\u002Fguide\u002Fenqueue","ジョブの投入",", ",[93,395,397],{"href":396},"\u002Fguide\u002Fexecution","実行の制御",[93,399,401],{"href":400},"\u002Fguide\u002Fschedule","定期実行",[93,403,405],{"href":404},"\u002Fguide\u002Fdashboard","ダッシュボードと認証",[28,407,408],{"id":408},"前提条件",[14,410,411,412,415,416,418,419,422,423,425,426,430],{},"Workers Paidプランと、2025-11-17以降の",[51,413,414],{},"compatibility_date","が必要です。",[18,417],{},"\n必要なリソースと設定は",[51,420,421],{},"npx tsumugi init","で作成します。",[18,424],{},"\n手順は",[93,427,429],{"href":428},"\u002Fguide\u002Fgetting-started","Getting Started","を参照してください。",[28,432,433],{"id":433},"着想",[14,435,436,437,443,444,446],{},"Cloudflareのサービスで完結するジョブキューに、ダッシュボードを同梱するという構成は、",[93,438,442],{"href":439,"rel":440},"https:\u002F\u002Fkiribi.pages.dev\u002F",[441],"nofollow","Kiribi","から着想を得ました。",[18,445],{},"\n導入の手順を最小限にし、利用者に内部構成を意識させない方針も、Kiribiと共通しています。",[14,448,449,450,452],{},"Tsumugiは、実行量の制限, ジョブごとの実行保証, リトライの制御, Flowによる多段の実行, 定期実行を備えています。",[18,451],{},"\nこれらの機能により、利用者が用意するリソースと設定は、Kiribiに比べて多くなります。",[454,455,456],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":135,"searchDepth":178,"depth":178,"links":458},[459,465,466,467],{"id":30,"depth":163,"text":30,"children":460},[461,462,463,464],{"id":37,"depth":178,"text":38},{"id":57,"depth":178,"text":57},{"id":79,"depth":178,"text":79},{"id":100,"depth":178,"text":100},{"id":125,"depth":163,"text":125},{"id":408,"depth":163,"text":408},{"id":433,"depth":163,"text":433},"Cloudflare Workers向けのジョブマネジメントシステムTsumugiが解決する課題, 利用者が記述するコード, 前提条件","md",{},"\u002Fguide\u002Foverview","2026-07-22",{"title":6,"description":468},"guide\u002Foverview","2026-10-25","DH0I5UZ-zdaNWtQpKZ4CnRtShlpO7fQ1gn8PvsqlpDg",false,1794027568266]