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