[{"data":1,"prerenderedAt":1421},["ShallowReactive",2],{"navigation_docs_en":3,"-en-async-scheduler":342,"-en-async-scheduler-surround":1416},[4,45,61,96,132,166,196,217,241,265],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Getting Started","i-lucide-rocket","\u002Fen\u002Fgetting-started","en\u002F1.getting-started",[10,15,20,24,29,34,39],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fgetting-started\u002Fintroduction","en\u002F1.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Coming from Laravel","\u002Fen\u002Fgetting-started\u002Fcoming-from-laravel","en\u002F1.getting-started\u002F2.coming-from-laravel","i-lucide-arrow-right-left",{"title":21,"path":22,"stem":23,"icon":19},"Coming from Symfony","\u002Fen\u002Fgetting-started\u002Fcoming-from-symfony","en\u002F1.getting-started\u002F3.coming-from-symfony",{"title":25,"path":26,"stem":27,"icon":28},"Installation","\u002Fen\u002Fgetting-started\u002Finstallation","en\u002F1.getting-started\u002F4.installation","i-lucide-download",{"title":30,"path":31,"stem":32,"icon":33},"Your first module","\u002Fen\u002Fgetting-started\u002Ffirst-module","en\u002F1.getting-started\u002F5.first-module","i-lucide-package-plus",{"title":35,"path":36,"stem":37,"icon":38},"Project structure","\u002Fen\u002Fgetting-started\u002Fproject-structure","en\u002F1.getting-started\u002F6.project-structure","i-lucide-folder-tree",{"title":40,"path":41,"stem":42,"icon":43},"The shop sample application","\u002Fen\u002Fgetting-started\u002Fsample-app","en\u002F1.getting-started\u002F7.sample-app","i-lucide-shopping-cart",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"Reference","i-lucide-book-open","\u002Fen\u002Freference","en\u002F10.reference",[51,56],{"title":52,"path":53,"stem":54,"icon":55},"Configuration","\u002Fen\u002Freference\u002Fconfiguration","en\u002F10.reference\u002F1.configuration","i-lucide-settings",{"title":57,"path":58,"stem":59,"icon":60},"External dependencies","\u002Fen\u002Freference\u002Fdependencies","en\u002F10.reference\u002F2.dependencies","i-lucide-package",{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":44},"Concepts","i-lucide-lightbulb","\u002Fen\u002Fconcepts","en\u002F2.concepts",[67,72,77,82,86,91],{"title":68,"path":69,"stem":70,"icon":71},"Architecture","\u002Fen\u002Fconcepts\u002Farchitecture","en\u002F2.concepts\u002F1.architecture","i-lucide-layers",{"title":73,"path":74,"stem":75,"icon":76},"Application lifecycle","\u002Fen\u002Fconcepts\u002Fapplication-lifecycle","en\u002F2.concepts\u002F2.application-lifecycle","i-lucide-power",{"title":78,"path":79,"stem":80,"icon":81},"Error model","\u002Fen\u002Fconcepts\u002Ferror-model","en\u002F2.concepts\u002F3.error-model","i-lucide-shield-alert",{"title":52,"path":83,"stem":84,"icon":85},"\u002Fen\u002Fconcepts\u002Fconfiguration","en\u002F2.concepts\u002F4.configuration","i-lucide-settings-2",{"title":87,"path":88,"stem":89,"icon":90},"Codegen pipeline","\u002Fen\u002Fconcepts\u002Fcodegen-pipeline","en\u002F2.concepts\u002F5.codegen-pipeline","i-lucide-file-json",{"title":92,"path":93,"stem":94,"icon":95},"Design patterns","\u002Fen\u002Fconcepts\u002Fdesign-patterns","en\u002F2.concepts\u002F6.design-patterns","i-lucide-puzzle",{"title":97,"icon":98,"path":99,"stem":100,"children":101,"page":44},"HTTP & Routing","i-lucide-globe","\u002Fen\u002Fhttp","en\u002F3.http",[102,107,112,117,122,127],{"title":103,"path":104,"stem":105,"icon":106},"The server core","\u002Fen\u002Fhttp\u002Fserver","en\u002F3.http\u002F1.server","i-lucide-server",{"title":108,"path":109,"stem":110,"icon":111},"The Fiber engine","\u002Fen\u002Fhttp\u002Ffiber","en\u002F3.http\u002F2.fiber","i-lucide-zap",{"title":113,"path":114,"stem":115,"icon":116},"The chi engine","\u002Fen\u002Fhttp\u002Fchi","en\u002F3.http\u002F3.chi","i-lucide-route",{"title":118,"path":119,"stem":120,"icon":121},"Error responses","\u002Fen\u002Fhttp\u002Ferror-responses","en\u002F3.http\u002F4.error-responses","i-lucide-octagon-alert",{"title":123,"path":124,"stem":125,"icon":126},"Validation","\u002Fen\u002Fhttp\u002Fvalidation","en\u002F3.http\u002F5.validation","i-lucide-badge-check",{"title":128,"path":129,"stem":130,"icon":131},"Pagination","\u002Fen\u002Fhttp\u002Fpagination","en\u002F3.http\u002F6.pagination","i-lucide-list-ordered",{"title":133,"icon":134,"path":135,"stem":136,"children":137,"page":44},"Database","i-lucide-database","\u002Fen\u002Fdatabase","en\u002F4.database",[138,142,147,151,156,161],{"title":139,"path":140,"stem":141,"icon":134},"Overview","\u002Fen\u002Fdatabase\u002Foverview","en\u002F4.database\u002F1.overview",{"title":143,"path":144,"stem":145,"icon":146},"pgx","\u002Fen\u002Fdatabase\u002Fpgx","en\u002F4.database\u002F2.pgx","i-lucide-plug",{"title":148,"path":149,"stem":150,"icon":71},"bun","\u002Fen\u002Fdatabase\u002Fbun","en\u002F4.database\u002F3.bun",{"title":152,"path":153,"stem":154,"icon":155},"Transactions","\u002Fen\u002Fdatabase\u002Ftransactions","en\u002F4.database\u002F4.transactions","i-lucide-git-merge",{"title":157,"path":158,"stem":159,"icon":160},"Migrations","\u002Fen\u002Fdatabase\u002Fmigrations","en\u002F4.database\u002F5.migrations","i-lucide-file-stack",{"title":162,"path":163,"stem":164,"icon":165},"Seeders","\u002Fen\u002Fdatabase\u002Fseeding","en\u002F4.database\u002F6.seeding","i-lucide-sprout",{"title":167,"icon":168,"path":169,"stem":170,"children":171,"page":44},"Async & Background","i-lucide-workflow","\u002Fen\u002Fasync","en\u002F5.async",[172,176,181,186,191],{"title":173,"path":174,"stem":175,"icon":131},"Queue","\u002Fen\u002Fasync\u002Fqueue","en\u002F5.async\u002F1.queue",{"title":177,"path":178,"stem":179,"icon":180},"Events","\u002Fen\u002Fasync\u002Fevents","en\u002F5.async\u002F2.events","i-lucide-radio",{"title":182,"path":183,"stem":184,"icon":185},"Outbox","\u002Fen\u002Fasync\u002Foutbox","en\u002F5.async\u002F3.outbox","i-lucide-inbox",{"title":187,"path":188,"stem":189,"icon":190},"Scheduling","\u002Fen\u002Fasync\u002Fscheduler","en\u002F5.async\u002F4.scheduler","i-lucide-calendar-clock",{"title":192,"path":193,"stem":194,"icon":195},"Worker","\u002Fen\u002Fasync\u002Fworker","en\u002F5.async\u002F5.worker","i-lucide-cog",{"title":197,"icon":198,"path":199,"stem":200,"children":201,"page":44},"Security","i-lucide-shield-check","\u002Fen\u002Fsecurity","en\u002F6.security",[202,207,212],{"title":203,"path":204,"stem":205,"icon":206},"Authentication","\u002Fen\u002Fsecurity\u002Fauthentication","en\u002F6.security\u002F1.authentication","i-lucide-key-round",{"title":208,"path":209,"stem":210,"icon":211},"RBAC","\u002Fen\u002Fsecurity\u002Frbac","en\u002F6.security\u002F2.rbac","i-lucide-users",{"title":213,"path":214,"stem":215,"icon":216},"Policies","\u002Fen\u002Fsecurity\u002Fpolicies","en\u002F6.security\u002F3.policies","i-lucide-gavel",{"title":218,"icon":219,"path":220,"stem":221,"children":222,"page":44},"Observability","i-lucide-activity","\u002Fen\u002Fobservability","en\u002F7.observability",[223,226,231,236],{"title":139,"path":224,"stem":225,"icon":219},"\u002Fen\u002Fobservability\u002Foverview","en\u002F7.observability\u002F1.overview",{"title":227,"path":228,"stem":229,"icon":230},"Logging","\u002Fen\u002Fobservability\u002Flogging","en\u002F7.observability\u002F2.logging","i-lucide-scroll-text",{"title":232,"path":233,"stem":234,"icon":235},"OpenTelemetry","\u002Fen\u002Fobservability\u002Fopentelemetry","en\u002F7.observability\u002F3.opentelemetry","i-lucide-radar",{"title":237,"path":238,"stem":239,"icon":240},"Sentry","\u002Fen\u002Fobservability\u002Fsentry","en\u002F7.observability\u002F4.sentry","i-lucide-bug",{"title":242,"icon":243,"path":244,"stem":245,"children":246,"page":44},"Storage & Mail","i-lucide-hard-drive","\u002Fen\u002Fstorage-mail","en\u002F8.storage-mail",[247,251,256,261],{"title":248,"path":249,"stem":250,"icon":243},"Storage","\u002Fen\u002Fstorage-mail\u002Fstorage","en\u002F8.storage-mail\u002F1.storage",{"title":252,"path":253,"stem":254,"icon":255},"Email","\u002Fen\u002Fstorage-mail\u002Fmail","en\u002F8.storage-mail\u002F2.mail","i-lucide-mail",{"title":257,"path":258,"stem":259,"icon":260},"Notifications","\u002Fen\u002Fstorage-mail\u002Fnotifications","en\u002F8.storage-mail\u002F3.notifications","i-lucide-bell",{"title":262,"path":263,"stem":264,"icon":180},"Realtime","\u002Fen\u002Fstorage-mail\u002Frealtime","en\u002F8.storage-mail\u002F4.realtime",{"title":266,"icon":267,"path":268,"stem":269,"children":270,"page":44},"CLI Reference","i-lucide-terminal","\u002Fen\u002Fcli","en\u002F9.cli",[271,274,279,284,289,294,299,303,308,313,318,323,328,333,337],{"title":139,"path":272,"stem":273,"icon":267},"\u002Fen\u002Fcli\u002Foverview","en\u002F9.cli\u002F1.overview",{"title":275,"path":276,"stem":277,"icon":278},"add db","\u002Fen\u002Fcli\u002Fadd-db","en\u002F9.cli\u002F10.add-db","i-lucide-database-zap",{"title":280,"path":281,"stem":282,"icon":283},"add compose \u002F add docker","\u002Fen\u002Fcli\u002Fadd-compose","en\u002F9.cli\u002F11.add-compose","i-lucide-container",{"title":285,"path":286,"stem":287,"icon":288},"add mail","\u002Fen\u002Fcli\u002Fadd-mail","en\u002F9.cli\u002F12.add-mail","i-lucide-mail-plus",{"title":290,"path":291,"stem":292,"icon":293},"add notification","\u002Fen\u002Fcli\u002Fadd-notification","en\u002F9.cli\u002F13.add-notification","i-lucide-bell-plus",{"title":295,"path":296,"stem":297,"icon":298},"add realtime","\u002Fen\u002Fcli\u002Fadd-realtime","en\u002F9.cli\u002F14.add-realtime","i-lucide-radio-tower",{"title":300,"path":301,"stem":302,"icon":165},"add seeder","\u002Fen\u002Fcli\u002Fadd-seeder","en\u002F9.cli\u002F15.add-seeder",{"title":304,"path":305,"stem":306,"icon":307},"new project","\u002Fen\u002Fcli\u002Fnew-project","en\u002F9.cli\u002F2.new-project","i-lucide-folder-plus",{"title":309,"path":310,"stem":311,"icon":312},"new module","\u002Fen\u002Fcli\u002Fnew-module","en\u002F9.cli\u002F3.new-module","i-lucide-blocks",{"title":314,"path":315,"stem":316,"icon":317},"add surface","\u002Fen\u002Fcli\u002Fadd-surface","en\u002F9.cli\u002F4.add-surface","i-lucide-layers-2",{"title":319,"path":320,"stem":321,"icon":322},"add core","\u002Fen\u002Fcli\u002Fadd-core","en\u002F9.cli\u002F5.add-core","i-lucide-box",{"title":324,"path":325,"stem":326,"icon":327},"add handler","\u002Fen\u002Fcli\u002Fadd-handler","en\u002F9.cli\u002F6.add-handler","i-lucide-webhook",{"title":329,"path":330,"stem":331,"icon":332},"new migration","\u002Fen\u002Fcli\u002Fnew-migration","en\u002F9.cli\u002F7.new-migration","i-lucide-file-plus",{"title":334,"path":335,"stem":336,"icon":195},"worker generators","\u002Fen\u002Fcli\u002Fworker-generators","en\u002F9.cli\u002F8.worker-generators",{"title":338,"path":339,"stem":340,"icon":341},"upgrade templates","\u002Fen\u002Fcli\u002Fupgrade-templates","en\u002F9.cli\u002F9.upgrade-templates","i-lucide-refresh-cw",{"id":343,"title":187,"body":344,"description":1409,"extension":1410,"links":1411,"meta":1412,"navigation":1413,"path":188,"seo":1414,"stem":189,"__hash__":1415},"docs_en\u002Fen\u002F5.async\u002F4.scheduler.md",{"type":345,"value":346,"toc":1400},"minimark",[347,377,401,404,427,432,447,623,626,841,848,852,862,967,978,1104,1123,1134,1138,1148,1156,1170,1174,1191,1221,1233,1278,1282,1301,1304,1321,1380,1384,1396],[348,349,354],"pre",{"className":350,"code":351,"language":352,"meta":353,"style":353},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import \"github.com\u002Fgp-system\u002Fgpsystem\u002Fscheduler\"\n","go","",[355,356,357],"code",{"__ignoreMap":353},[358,359,362,366,370,374],"span",{"class":360,"line":361},"line",1,[358,363,365],{"class":364},"s7zQu","import",[358,367,369],{"class":368},"sMK4o"," \"",[358,371,373],{"class":372},"sBMFI","github.com\u002Fgp-system\u002Fgpsystem\u002Fscheduler",[358,375,376],{"class":368},"\"\n",[378,379,380,383,384,387,388,391,392,396,397,400],"p",{},[355,381,382],{},"scheduler"," lets you define your schedule in ",[385,386,355],"strong",{},", not a crontab: you build a ",[355,389,390],{},"Schedule"," fluently and the ",[393,394,395],"a",{"href":193},"worker"," runs it. Running on a single server among replicas is the ",[385,398,399],{},"default"," behavior here, not an opt-in: among N worker replicas, a Redis-lease leader election decides which one fires, with no extra flag and no cache lock to configure.",[378,402,403],{},"You can schedule two kinds of things:",[405,406,407,420],"ul",{},[408,409,410,411,414,415,419],"li",{},"an ",[385,412,413],{},"event",": a fire is a plain ",[393,416,418],{"href":417},"\u002Fen\u002Fasync\u002Fevents#dispatch","dispatch",", the event fans out to all of its listeners;",[408,421,422,423,426],{},"a ",[385,424,425],{},"job",": a single named handler, no fan-out.",[428,429,431],"h2",{"id":430},"building-the-schedule","Building the schedule",[378,433,434,435,438,439,442,443,446],{},"Every module's generated ",[355,436,437],{},"jobs\u002Fregister.go"," provides a ",[355,440,441],{},"Register(sched, deps)","; the worker composes them into a single schedule. The shop's nightly report was generated by ",[355,444,445],{},"add job shop nightlySalesReport --cron \"0 3 * * *\"",":",[348,448,451],{"className":350,"code":449,"filename":450,"language":352,"meta":353,"style":353},"package jobs\n\nimport (\n    \"github.com\u002Facme\u002Fshop\u002Finternal\u002Fmodules\u002Fshop\"\n\n    \"github.com\u002Fgp-system\u002Fgpsystem\u002Fscheduler\"\n    \u002F\u002F gpsystem:job-imports\n)\n\nfunc Register(sched *scheduler.Schedule, deps shop.Dependencies) {\n    sched.Job(\"shop.nightlySalesReport\", \"0 3 * * *\", nightlySalesReport(deps))\n    \u002F\u002F gpsystem:jobs\n}\n","internal\u002Fmodules\u002Fshop\u002Fjobs\u002Fregister.go",[355,452,453,461,468,476,487,492,501,508,514,519,566,611,617],{"__ignoreMap":353},[358,454,455,458],{"class":360,"line":361},[358,456,457],{"class":368},"package",[358,459,460],{"class":372}," jobs\n",[358,462,464],{"class":360,"line":463},2,[358,465,467],{"emptyLinePlaceholder":466},true,"\n",[358,469,471,473],{"class":360,"line":470},3,[358,472,365],{"class":364},[358,474,475],{"class":368}," (\n",[358,477,479,482,485],{"class":360,"line":478},4,[358,480,481],{"class":368},"    \"",[358,483,484],{"class":372},"github.com\u002Facme\u002Fshop\u002Finternal\u002Fmodules\u002Fshop",[358,486,376],{"class":368},[358,488,490],{"class":360,"line":489},5,[358,491,467],{"emptyLinePlaceholder":466},[358,493,495,497,499],{"class":360,"line":494},6,[358,496,481],{"class":368},[358,498,373],{"class":372},[358,500,376],{"class":368},[358,502,504],{"class":360,"line":503},7,[358,505,507],{"class":506},"sHwdD","    \u002F\u002F gpsystem:job-imports\n",[358,509,511],{"class":360,"line":510},8,[358,512,513],{"class":368},")\n",[358,515,517],{"class":360,"line":516},9,[358,518,467],{"emptyLinePlaceholder":466},[358,520,522,525,529,532,536,539,541,544,546,549,552,555,557,560,563],{"class":360,"line":521},10,[358,523,524],{"class":368},"func",[358,526,528],{"class":527},"s2Zo4"," Register",[358,530,531],{"class":368},"(",[358,533,535],{"class":534},"sHdIc","sched",[358,537,538],{"class":368}," *",[358,540,382],{"class":372},[358,542,543],{"class":368},".",[358,545,390],{"class":372},[358,547,548],{"class":368},",",[358,550,551],{"class":534}," deps",[358,553,554],{"class":372}," shop",[358,556,543],{"class":368},[358,558,559],{"class":372},"Dependencies",[358,561,562],{"class":368},")",[358,564,565],{"class":368}," {\n",[358,567,569,573,575,578,580,583,587,589,591,593,596,598,600,603,605,608],{"class":360,"line":568},11,[358,570,572],{"class":571},"sTEyZ","    sched",[358,574,543],{"class":368},[358,576,577],{"class":527},"Job",[358,579,531],{"class":368},[358,581,582],{"class":368},"\"",[358,584,586],{"class":585},"sfazB","shop.nightlySalesReport",[358,588,582],{"class":368},[358,590,548],{"class":368},[358,592,369],{"class":368},[358,594,595],{"class":585},"0 3 * * *",[358,597,582],{"class":368},[358,599,548],{"class":368},[358,601,602],{"class":527}," nightlySalesReport",[358,604,531],{"class":368},[358,606,607],{"class":571},"deps",[358,609,610],{"class":368},"))\n",[358,612,614],{"class":360,"line":613},12,[358,615,616],{"class":506},"    \u002F\u002F gpsystem:jobs\n",[358,618,620],{"class":360,"line":619},13,[358,621,622],{"class":368},"}\n",[378,624,625],{},"The job's handler stub lives in its own file; the body is yours:",[348,627,630],{"className":350,"code":628,"filename":629,"language":352,"meta":353,"style":353},"package jobs\n\nimport (\n    \"context\"\n\n    \"github.com\u002Facme\u002Fshop\u002Finternal\u002Fmodules\u002Fshop\"\n)\n\nfunc nightlySalesReport(deps shop.Dependencies) func(context.Context) error {\n    return func(ctx context.Context) error {\n        report, err := buildSalesReport(ctx, deps.DB)\n        if err != nil {\n            return err\n        }\n        return deps.Mailer.Send(ctx, salesReportMessage(report))\n    }\n}\n","internal\u002Fmodules\u002Fshop\u002Fjobs\u002Fnightly_sales_report.go",[355,631,632,638,642,648,657,661,669,673,677,713,736,767,782,790,796,830,836],{"__ignoreMap":353},[358,633,634,636],{"class":360,"line":361},[358,635,457],{"class":368},[358,637,460],{"class":372},[358,639,640],{"class":360,"line":463},[358,641,467],{"emptyLinePlaceholder":466},[358,643,644,646],{"class":360,"line":470},[358,645,365],{"class":364},[358,647,475],{"class":368},[358,649,650,652,655],{"class":360,"line":478},[358,651,481],{"class":368},[358,653,654],{"class":372},"context",[358,656,376],{"class":368},[358,658,659],{"class":360,"line":489},[358,660,467],{"emptyLinePlaceholder":466},[358,662,663,665,667],{"class":360,"line":494},[358,664,481],{"class":368},[358,666,484],{"class":372},[358,668,376],{"class":368},[358,670,671],{"class":360,"line":503},[358,672,513],{"class":368},[358,674,675],{"class":360,"line":510},[358,676,467],{"emptyLinePlaceholder":466},[358,678,679,681,683,685,687,689,691,693,695,698,700,702,705,707,711],{"class":360,"line":516},[358,680,524],{"class":368},[358,682,602],{"class":527},[358,684,531],{"class":368},[358,686,607],{"class":534},[358,688,554],{"class":372},[358,690,543],{"class":368},[358,692,559],{"class":372},[358,694,562],{"class":368},[358,696,697],{"class":368}," func(",[358,699,654],{"class":372},[358,701,543],{"class":368},[358,703,704],{"class":372},"Context",[358,706,562],{"class":368},[358,708,710],{"class":709},"spNyl"," error",[358,712,565],{"class":368},[358,714,715,718,720,723,726,728,730,732,734],{"class":360,"line":521},[358,716,717],{"class":364},"    return",[358,719,697],{"class":368},[358,721,722],{"class":534},"ctx",[358,724,725],{"class":372}," context",[358,727,543],{"class":368},[358,729,704],{"class":372},[358,731,562],{"class":368},[358,733,710],{"class":709},[358,735,565],{"class":368},[358,737,738,741,743,746,749,752,754,756,758,760,762,765],{"class":360,"line":568},[358,739,740],{"class":571},"        report",[358,742,548],{"class":368},[358,744,745],{"class":571}," err ",[358,747,748],{"class":368},":=",[358,750,751],{"class":527}," buildSalesReport",[358,753,531],{"class":368},[358,755,722],{"class":571},[358,757,548],{"class":368},[358,759,551],{"class":571},[358,761,543],{"class":368},[358,763,764],{"class":571},"DB",[358,766,513],{"class":368},[358,768,769,772,774,777,780],{"class":360,"line":613},[358,770,771],{"class":364},"        if",[358,773,745],{"class":571},[358,775,776],{"class":368},"!=",[358,778,779],{"class":368}," nil",[358,781,565],{"class":368},[358,783,784,787],{"class":360,"line":619},[358,785,786],{"class":364},"            return",[358,788,789],{"class":571}," err\n",[358,791,793],{"class":360,"line":792},14,[358,794,795],{"class":368},"        }\n",[358,797,799,802,804,806,809,811,814,816,818,820,823,825,828],{"class":360,"line":798},15,[358,800,801],{"class":364},"        return",[358,803,551],{"class":571},[358,805,543],{"class":368},[358,807,808],{"class":571},"Mailer",[358,810,543],{"class":368},[358,812,813],{"class":527},"Send",[358,815,531],{"class":368},[358,817,722],{"class":571},[358,819,548],{"class":368},[358,821,822],{"class":527}," salesReportMessage",[358,824,531],{"class":368},[358,826,827],{"class":571},"report",[358,829,610],{"class":368},[358,831,833],{"class":360,"line":832},16,[358,834,835],{"class":368},"    }\n",[358,837,839],{"class":360,"line":838},17,[358,840,622],{"class":368},[378,842,843,844,847],{},"A returned error triggers a retry (up to the default budget of 25, or the entry's own ",[355,845,846],{},"queue.MaxRetry","); a transient DB error doesn't cost you the report.",[428,849,851],{"id":850},"event-schedules-vs-job-schedules","Event schedules vs job schedules",[378,853,854,855,857,858,861],{},"The ",[355,856,390],{}," fluent API: every method returns ",[355,859,860],{},"*Schedule",", so calls chain.",[863,864,865,878],"table",{},[866,867,868],"thead",{},[869,870,871,875],"tr",{},[872,873,874],"th",{},"Method",[872,876,877],{},"Fires",[879,880,881,892,910,920,930,943,953],"tbody",{},[869,882,883,889],{},[884,885,886],"td",{},[355,887,888],{},"Cron(spec, ev, opts...)",[884,890,891],{},"on a cron expression",[869,893,894,899],{},[884,895,896],{},[355,897,898],{},"Every(d, ev, opts...)",[884,900,901,902,905,906,909],{},"every ",[355,903,904],{},"d"," (translates to an ",[355,907,908],{},"@every"," descriptor)",[869,911,912,917],{},[884,913,914],{},[355,915,916],{},"EveryMinute(ev, opts...)",[884,918,919],{},"at the top of every minute",[869,921,922,927],{},[884,923,924],{},[355,925,926],{},"Hourly(ev, opts...)",[884,928,929],{},"at the top of every hour",[869,931,932,937],{},[884,933,934],{},[355,935,936],{},"Daily(ev, opts...)",[884,938,939,940],{},"at midnight, per ",[355,941,942],{},"SCHEDULER_TIMEZONE",[869,944,945,950],{},[884,946,947],{},[355,948,949],{},"DailyAt(\"HH:MM\", ev, opts...)",[884,951,952],{},"daily at the given time (24-hour; panics at setup on a malformed value)",[869,954,955,960],{},[884,956,957],{},[355,958,959],{},"Job(name, spec, fn, opts...)",[884,961,962,963,966],{},"runs ",[355,964,965],{},"fn func(context.Context) error"," on a cron",[378,968,969,970,973,974,977],{},"Which one when? An ",[385,971,972],{},"event schedule"," when the fire needs several independently retried reactions, or when the same event is also dispatched at runtime: the listeners don't know and don't need to know whether a cron or a user action dispatched it. A ",[385,975,976],{},"job schedule"," when it is one named piece of work, like the shop's report.",[348,979,981],{"className":350,"code":980,"language":352,"meta":353,"style":353},"\u002F\u002F Event schedule: every fire dispatches a fresh event that fans out\n\u002F\u002F to all of its listeners, exactly like a runtime Dispatch.\nsched.Every(15*time.Minute, shopevents.SyncInventory{})\nsched.DailyAt(\"06:30\", shopevents.DigestDue{})\n\n\u002F\u002F Job schedule: one handler, no fan-out; the task type is \"job:\u003Cname>\".\nsched.Job(\"shop.nightlySalesReport\", \"0 3 * * *\", nightlySalesReport(deps))\n",[355,982,983,988,993,1032,1061,1065,1070],{"__ignoreMap":353},[358,984,985],{"class":360,"line":361},[358,986,987],{"class":506},"\u002F\u002F Event schedule: every fire dispatches a fresh event that fans out\n",[358,989,990],{"class":360,"line":463},[358,991,992],{"class":506},"\u002F\u002F to all of its listeners, exactly like a runtime Dispatch.\n",[358,994,995,997,999,1002,1004,1008,1011,1014,1016,1019,1021,1024,1026,1029],{"class":360,"line":470},[358,996,535],{"class":571},[358,998,543],{"class":368},[358,1000,1001],{"class":527},"Every",[358,1003,531],{"class":368},[358,1005,1007],{"class":1006},"sbssI","15",[358,1009,1010],{"class":368},"*",[358,1012,1013],{"class":571},"time",[358,1015,543],{"class":368},[358,1017,1018],{"class":571},"Minute",[358,1020,548],{"class":368},[358,1022,1023],{"class":372}," shopevents",[358,1025,543],{"class":368},[358,1027,1028],{"class":372},"SyncInventory",[358,1030,1031],{"class":368},"{})\n",[358,1033,1034,1036,1038,1041,1043,1045,1048,1050,1052,1054,1056,1059],{"class":360,"line":478},[358,1035,535],{"class":571},[358,1037,543],{"class":368},[358,1039,1040],{"class":527},"DailyAt",[358,1042,531],{"class":368},[358,1044,582],{"class":368},[358,1046,1047],{"class":585},"06:30",[358,1049,582],{"class":368},[358,1051,548],{"class":368},[358,1053,1023],{"class":372},[358,1055,543],{"class":368},[358,1057,1058],{"class":372},"DigestDue",[358,1060,1031],{"class":368},[358,1062,1063],{"class":360,"line":489},[358,1064,467],{"emptyLinePlaceholder":466},[358,1066,1067],{"class":360,"line":494},[358,1068,1069],{"class":506},"\u002F\u002F Job schedule: one handler, no fan-out; the task type is \"job:\u003Cname>\".\n",[358,1071,1072,1074,1076,1078,1080,1082,1084,1086,1088,1090,1092,1094,1096,1098,1100,1102],{"class":360,"line":503},[358,1073,535],{"class":571},[358,1075,543],{"class":368},[358,1077,577],{"class":527},[358,1079,531],{"class":368},[358,1081,582],{"class":368},[358,1083,586],{"class":585},[358,1085,582],{"class":368},[358,1087,548],{"class":368},[358,1089,369],{"class":368},[358,1091,595],{"class":585},[358,1093,582],{"class":368},[358,1095,548],{"class":368},[358,1097,602],{"class":527},[358,1099,531],{"class":368},[358,1101,607],{"class":571},[358,1103,610],{"class":368},[378,1105,854,1106,1109,1110,1114,1115,1118,1119,1122],{},[355,1107,1108],{},"opts..."," are the usual ",[393,1111,1113],{"href":1112},"\u002Fen\u002Fasync\u002Fqueue#enqueue-options","enqueue options"," (e.g. ",[355,1116,1117],{},"queue.OnQueue(\"reports\")",", ",[355,1120,1121],{},"queue.Timeout(10*time.Minute)","), and apply to the task submitted on each fire.",[378,1124,1125,1126,1129,1130,1133],{},"One technical detail that provides the guarantees: when a scheduled event fires, the worker uses the trigger task's id as the envelope id. So ",[385,1127,1128],{},"every fire is a new dispatch"," (every listener receives it again), but ",[385,1131,1132],{},"a retry of the same fire is idempotent",": it doesn't double the fan-out.",[428,1135,1137],{"id":1136},"cron-syntax","Cron syntax",[378,1139,854,1140,1143,1144,1147],{},[355,1141,1142],{},"spec"," is the standard 5-field cron (",[355,1145,1146],{},"minute hour day-of-month month day-of-week","), plus asynq's descriptors:",[348,1149,1154],{"className":1150,"code":1152,"language":1153},[1151],"language-text","\"0 3 * * *\"     every day at 03:00\n\"*\u002F10 * * * *\"  every 10 minutes\n\"0 8 * * 1\"     Mondays at 08:00\n\"@every 30m\"    every 30 minutes (not on the clock grid, counted from start)\n\"@daily\"        at midnight\n","text",[355,1155,1152],{"__ignoreMap":353},[378,1157,1158,1159,1161,1162,1165,1166,1169],{},"Expressions are evaluated in ",[355,1160,942],{}," (an IANA name, e.g. ",[355,1163,1164],{},"Europe\u002FBudapest","); the default is UTC, so the shop's ",[355,1167,1168],{},"\"0 3 * * *\""," is 03:00 UTC.",[428,1171,1173],{"id":1172},"replica-safety-the-leader-election","Replica safety: the leader election",[378,1175,1176,1177,1180,1181,1186,1187,1190],{},"The schedule is executed by the ",[355,1178,1179],{},"Runner",", which you never construct by hand: ",[393,1182,1183],{"href":193},[355,1184,1185],{},"worker.Run"," starts it automatically when the schedule is non-empty. Inside, asynq's ",[355,1188,1189],{},"Scheduler"," does the work, guarded by a Redis lease:",[1192,1193,1194,1205,1215],"ol",{},[408,1195,1196,1197,1200,1201,1204],{},"Every worker replica tries to acquire the lease key (",[355,1198,1199],{},"SETNX",", expiring after ",[355,1202,1203],{},"SCHEDULER_LEASE_TTL",").",[408,1206,1207,1210,1211,1214],{},[385,1208,1209],{},"Only the lease holder"," registers and runs the cron entries; it renews the lease every ",[355,1212,1213],{},"LEASE_TTL\u002F3"," (a Lua compare-and-act, so a stale old leader cannot clobber the new one).",[408,1216,1217,1218,543],{},"If the leader dies or loses the lease, its firing stops, and another replica takes over after at most ",[355,1219,1220],{},"LEASE_TTL",[378,1222,1223,1224,1228,1229,1232],{},"A brief overlap is possible on handover (the old leader issued one more tick, the new one did too), but the rest of the system absorbs it: delivery is at-least-once anyway, and listeners are ",[393,1225,1227],{"href":1226},"\u002Fen\u002Fasync\u002Fevents#idempotency","idempotent",". When several applications (or environments) share one Redis, separate their lease keys with ",[355,1230,1231],{},"SCHEDULER_NAMESPACE","; otherwise they share each other's leader election.",[348,1234,1237],{"className":350,"code":1235,"filename":1236,"language":352,"meta":353,"style":353},"\u002F\u002F \"run on one server\" is not an option here, it's the only mode of operation\nsched.Job(\"shop.nightlySalesReport\", \"0 3 * * *\", nightlySalesReport(deps))\n","gpsystem: internal\u002Fmodules\u002Fshop\u002Fjobs\u002Fregister.go",[355,1238,1239,1244],{"__ignoreMap":353},[358,1240,1241],{"class":360,"line":361},[358,1242,1243],{"class":506},"\u002F\u002F \"run on one server\" is not an option here, it's the only mode of operation\n",[358,1245,1246,1248,1250,1252,1254,1256,1258,1260,1262,1264,1266,1268,1270,1272,1274,1276],{"class":360,"line":463},[358,1247,535],{"class":571},[358,1249,543],{"class":368},[358,1251,577],{"class":527},[358,1253,531],{"class":368},[358,1255,582],{"class":368},[358,1257,586],{"class":585},[358,1259,582],{"class":368},[358,1261,548],{"class":368},[358,1263,369],{"class":368},[358,1265,595],{"class":585},[358,1267,582],{"class":368},[358,1269,548],{"class":368},[358,1271,602],{"class":527},[358,1273,531],{"class":368},[358,1275,607],{"class":571},[358,1277,610],{"class":368},[428,1279,1281],{"id":1280},"what-runs-where","What runs where",[378,1283,1284,1285,1288,1289,1292,1293,1296,1297,1300],{},"An important difference from a crontab: firing and ",[385,1286,1287],{},"execution"," are decoupled. The leader only enqueues: the ",[355,1290,1291],{},"schedule:\u003Cevent>"," or ",[355,1294,1295],{},"job:\u003Cname>"," task lands on the queue, and ",[385,1298,1299],{},"any"," worker replica may process it, under the usual concurrency and queue weights. A heavy report therefore doesn't load \"the scheduler machine\"; it loads the pool.",[428,1302,52],{"id":1303},"configuration",[378,1305,1306,1309,1310,1316,1317,1320],{},[355,1307,1308],{},"scheduler.Config"," arrives embedded in ",[393,1311,1313],{"href":1312},"\u002Fen\u002Fasync\u002Fworker#configuration",[355,1314,1315],{},"worker.Config",", under the ",[355,1318,1319],{},"SCHEDULER_"," prefix:",[863,1322,1323,1336],{},[866,1324,1325],{},[869,1326,1327,1330,1333],{},[872,1328,1329],{},"Variable",[872,1331,1332],{},"Default",[872,1334,1335],{},"Meaning",[879,1337,1338,1352,1366],{},[869,1339,1340,1344,1349],{},[884,1341,1342],{},[355,1343,942],{},[884,1345,1346],{},[355,1347,1348],{},"UTC",[884,1350,1351],{},"IANA timezone the cron expressions are evaluated in",[869,1353,1354,1358,1363],{},[884,1355,1356],{},[355,1357,1203],{},[884,1359,1360],{},[355,1361,1362],{},"15s",[884,1364,1365],{},"the leader lease's TTL; the leader renews it every TTL\u002F3",[869,1367,1368,1372,1377],{},[884,1369,1370],{},[355,1371,1231],{},[884,1373,1374],{},[355,1375,1376],{},"gpsystem",[884,1378,1379],{},"namespace of the lease key; set it to your app's name when several apps share one Redis",[428,1381,1383],{"id":1382},"patterns-used","Patterns used",[378,1385,1386,1389,1390,1392,1393,543],{},[385,1387,1388],{},"Redis-lease leader election"," (",[355,1391,1199],{}," to acquire, a Lua compare-and-renew script to renew): see ",[393,1394,92],{"href":1395},"\u002Fen\u002Fconcepts\u002Fdesign-patterns#redis-lease-leader-election",[1397,1398,1399],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":353,"searchDepth":463,"depth":463,"links":1401},[1402,1403,1404,1405,1406,1407,1408],{"id":430,"depth":463,"text":431},{"id":850,"depth":463,"text":851},{"id":1136,"depth":463,"text":1137},{"id":1172,"depth":463,"text":1173},{"id":1280,"depth":463,"text":1281},{"id":1303,"depth":463,"text":52},{"id":1382,"depth":463,"text":1383},"Task scheduling in code, cron events and jobs, replica-safe with a Redis-lease leader election.","md",null,{},{"icon":190},{"title":187,"description":1409},"NHn2jgddhPPxiZcqU0gPHLVD-dxcicJKRq1Ju2PVhAs",[1417,1419],{"title":182,"path":183,"stem":184,"description":1418,"icon":185,"children":-1},"The transactional outbox pattern, the business write and the event commit atomically, with a delivery guarantee.",{"title":192,"path":193,"stem":194,"description":1420,"icon":195,"children":-1},"The background-work chassis, asynq server, outbox relay and schedule runner in one binary, under a shared graceful shutdown.",1784668703427]