[{"data":1,"prerenderedAt":1496},["ShallowReactive",2],{"navigation_docs":3,"-platform-reference-grind-jobs":213,"-platform-reference-grind-jobs-surround":1491},[4,142],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":36},"Kinotic Apps","i-lucide-rocket","\u002Fapps","01.apps",[10,14,18,37,58,91,102,122,127],{"title":11,"path":12,"stem":13},"Introduction","\u002Fapps\u002Fintroduction","01.apps\u002F01.introduction",{"title":15,"path":16,"stem":17},"Quick Start","\u002Fapps\u002Fquick-start","01.apps\u002F02.quick-start",{"title":19,"icon":20,"path":21,"stem":22,"children":23,"page":36},"Application Structure","i-lucide-folder-tree","\u002Fapps\u002Fapplication-structure","01.apps\u002F03.application-structure",[24,28,32],{"title":25,"path":26,"stem":27},"Overview","\u002Fapps\u002Fapplication-structure\u002Foverview","01.apps\u002F03.application-structure\u002F01.overview",{"title":29,"path":30,"stem":31},"Applications and Projects","\u002Fapps\u002Fapplication-structure\u002Fapplications-and-projects","01.apps\u002F03.application-structure\u002F02.applications-and-projects",{"title":33,"path":34,"stem":35},"Artifact Types","\u002Fapps\u002Fapplication-structure\u002Fartifact-types","01.apps\u002F03.application-structure\u002F03.artifact-types",false,{"title":38,"icon":39,"path":40,"stem":41,"children":42,"page":36},"Services","i-lucide-network","\u002Fapps\u002Fservices","01.apps\u002F04.services",[43,46,50,54],{"title":25,"path":44,"stem":45},"\u002Fapps\u002Fservices\u002Foverview","01.apps\u002F04.services\u002F01.overview",{"title":47,"path":48,"stem":49},"Publishing Services","\u002Fapps\u002Fservices\u002Fpublishing-services","01.apps\u002F04.services\u002F02.publishing-services",{"title":51,"path":52,"stem":53},"Service Proxies","\u002Fapps\u002Fservices\u002Fservice-proxies","01.apps\u002F04.services\u002F03.service-proxies",{"title":55,"path":56,"stem":57},"Streaming","\u002Fapps\u002Fservices\u002Fstreaming","01.apps\u002F04.services\u002F04.streaming",{"title":59,"icon":60,"path":61,"stem":62,"children":63,"page":36},"Persistence","i-lucide-database","\u002Fapps\u002Fpersistence","01.apps\u002F05.persistence",[64,67,71,75,79,83,87],{"title":25,"path":65,"stem":66},"\u002Fapps\u002Fpersistence\u002Foverview","01.apps\u002F05.persistence\u002F01.overview",{"title":68,"path":69,"stem":70},"Defining Entities","\u002Fapps\u002Fpersistence\u002Fdefining-entities","01.apps\u002F05.persistence\u002F02.defining-entities",{"title":72,"path":73,"stem":74},"Entity Decorators","\u002Fapps\u002Fpersistence\u002Fentity-decorators","01.apps\u002F05.persistence\u002F03.entity-decorators",{"title":76,"path":77,"stem":78},"CRUD Operations","\u002Fapps\u002Fpersistence\u002Fcrud-operations","01.apps\u002F05.persistence\u002F04.crud-operations",{"title":80,"path":81,"stem":82},"Named Queries","\u002Fapps\u002Fpersistence\u002Fnamed-queries","01.apps\u002F05.persistence\u002F05.named-queries",{"title":84,"path":85,"stem":86},"Multi-Tenancy","\u002Fapps\u002Fpersistence\u002Fmulti-tenancy","01.apps\u002F05.persistence\u002F06.multi-tenancy",{"title":88,"path":89,"stem":90},"Migrations","\u002Fapps\u002Fpersistence\u002Fmigrations","01.apps\u002F05.persistence\u002F07.migrations",{"title":92,"icon":93,"path":94,"stem":95,"children":96,"page":36},"Security","i-lucide-shield-check","\u002Fapps\u002Fsecurity","01.apps\u002F06.security",[97],{"title":98,"path":99,"stem":100,"icon":101},"Authentication","\u002Fapps\u002Fsecurity\u002Fauthentication","01.apps\u002F06.security\u002F01.authentication","i-lucide-key-round",{"title":103,"icon":104,"path":105,"stem":106,"children":107,"page":36},"Deployment","i-lucide-cloud-upload","\u002Fapps\u002Fdeployment","01.apps\u002F07.deployment",[108,113,118],{"title":109,"path":110,"stem":111,"icon":112},"Deployment Workflow","\u002Fapps\u002Fdeployment\u002Fworkflow","01.apps\u002F07.deployment\u002F01.workflow","i-lucide-git-branch",{"title":114,"path":115,"stem":116,"icon":117},"Environments","\u002Fapps\u002Fdeployment\u002Fenvironments","01.apps\u002F07.deployment\u002F02.environments","i-lucide-server",{"title":119,"path":120,"stem":121,"icon":6},"Push to Deploy","\u002Fapps\u002Fdeployment\u002Fpush-to-deploy","01.apps\u002F07.deployment\u002F03.push-to-deploy",{"title":123,"path":124,"stem":125,"icon":126},"CLI Reference","\u002Fapps\u002Fcli-reference","01.apps\u002F08.cli-reference","i-lucide-terminal",{"title":128,"icon":129,"path":130,"stem":131,"children":132,"page":36},"Reference","i-lucide-book-open","\u002Fapps\u002Freference","01.apps\u002F09.reference",[133,138],{"title":134,"path":135,"stem":136,"icon":137},"Decorators Reference","\u002Fapps\u002Freference\u002Fdecorators","01.apps\u002F09.reference\u002F01.decorators","i-lucide-at-sign",{"title":139,"path":140,"stem":141,"icon":60},"Migration SQL Grammar","\u002Fapps\u002Freference\u002Fmigration-sql-grammar","01.apps\u002F09.reference\u002F02.migration-sql-grammar",{"title":143,"icon":117,"path":144,"stem":145,"children":146,"page":36},"Kinotic OS","\u002Fplatform","02.platform",[147,152,156,161,166,171,175,180,185,190,195],{"title":148,"path":149,"stem":150,"icon":151},"System Architecture","\u002Fplatform\u002Farchitecture","02.platform\u002F01.architecture","i-lucide-boxes",{"title":153,"path":154,"stem":155,"icon":6},"Deployment Guide","\u002Fplatform\u002Fdeployment-guide","02.platform\u002F02.deployment-guide",{"title":157,"path":158,"stem":159,"icon":160},"Configuration","\u002Fplatform\u002Fconfiguration","02.platform\u002F03.configuration","i-lucide-settings",{"title":162,"path":163,"stem":164,"icon":165},"Organization Management","\u002Fplatform\u002Forganization-management","02.platform\u002F04.organization-management","i-lucide-building",{"title":167,"path":168,"stem":169,"icon":170},"System Security","\u002Fplatform\u002Fsystem-security","02.platform\u002F05.system-security","i-lucide-shield",{"title":172,"path":173,"stem":174,"icon":93},"Defense in Depth","\u002Fplatform\u002Fdefense-in-depth","02.platform\u002F06.defense-in-depth",{"title":176,"path":177,"stem":178,"icon":179},"MCP Tools","\u002Fplatform\u002Fmcp-tools","02.platform\u002F07.mcp-tools","i-lucide-bot",{"title":181,"path":182,"stem":183,"icon":184},"Observability","\u002Fplatform\u002Fobservability","02.platform\u002F08.observability","i-lucide-activity",{"title":186,"path":187,"stem":188,"icon":189},"Contributing","\u002Fplatform\u002Fcontributing","02.platform\u002F09.contributing","i-lucide-git-pull-request",{"title":191,"path":192,"stem":193,"icon":194},"System Migrations","\u002Fplatform\u002Fsystem-migrations","02.platform\u002F10.system-migrations","i-lucide-database-zap",{"title":128,"icon":129,"path":196,"stem":197,"children":198,"page":36},"\u002Fplatform\u002Freference","02.platform\u002F11.reference",[199,204,209],{"title":200,"path":201,"stem":202,"icon":203},"CRI Format","\u002Fplatform\u002Freference\u002Fcri-format","02.platform\u002F11.reference\u002F01.cri-format","i-lucide-link",{"title":205,"path":206,"stem":207,"icon":208},"Grind Jobs","\u002Fplatform\u002Freference\u002Fgrind-jobs","02.platform\u002F11.reference\u002F02.grind-jobs","i-lucide-workflow",{"title":210,"path":211,"stem":212,"icon":151},"Project Publishing Design","\u002Fplatform\u002Freference\u002Fproject-publishing-design","02.platform\u002F11.reference\u002F03.project-publishing-design",{"id":214,"title":205,"body":215,"description":1484,"extension":1485,"links":1486,"meta":1487,"navigation":1488,"path":206,"seo":1489,"stem":207,"__hash__":1490},"docs\u002F02.platform\u002F11.reference\u002F02.grind-jobs.md",{"type":216,"value":217,"toc":1468},"minimark",[218,222,244,248,258,402,416,446,451,465,508,512,531,592,595,599,620,624,630,665,675,721,744,756,760,784,788,803,812,823,832,855,927,937,941,947,956,973,995,1010,1057,1060,1067,1071,1078,1107,1113,1117,1130,1139,1149,1153,1187,1210,1214,1235,1321,1338,1454,1464],[219,220,25],"h2",{"id":221},"overview",[223,224,225,226,230,231,235,236,239,240,243],"p",{},"Grind (the ",[227,228,229],"code",{},"kinotic-grind"," module) is the platform's job framework: multi-step work such as a project deployment or a provisioning pipeline, expressed as a ",[232,233,234],"strong",{},"job"," of ",[232,237,238],{},"tasks"," that share a scope. Its core idea is declarative state injection — instead of threading an ever-growing context object through every step, each task declares what it consumes, injected from the job scope, and what it produces, stored back under a ",[227,241,242],{},"Store",". Every run is recorded, resumable after a failure, and observable as a live event stream.",[219,245,247],{"id":246},"defining-a-job","Defining a job",[223,249,250,251,254,255,257],{},"A ",[227,252,253],{},"JobDefinition"," is built fluently. Tasks execute in the order they are added, and each task's result is kept according to the ",[227,256,242],{}," given with it:",[259,260,265],"pre",{"className":261,"code":262,"language":263,"meta":264,"style":264},"language-java shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","JobDefinition job = JobDefinition.create(\"Deploy project\")\n        .name(\"project-deploy\").version(\"1\")\n        .input(project)                       \u002F\u002F seeds the scope before the first task\n        .task(Tasks.fromCallable(\"Resolve deploy target\", new Callable\u003CDeployTarget>() {\n\n            @Autowired\n            private Project project;          \u002F\u002F injected from the job scope\n\n            @Override\n            public DeployTarget call() {\n                return resolveTarget(project);\n            }\n        }), Store.state(\"deployTarget\"))\n        .task(Tasks.fromCallable(\"Sync project source\", new Callable\u003CString>() {\n\n            @Autowired\n            private DeployTarget target;      \u002F\u002F stored by the previous task\n\n            @Override\n            public String call() {\n                return syncSource(target);\n            }\n        }));\n","java","",[227,266,267,275,281,287,293,300,306,312,317,323,329,335,341,347,353,358,363,369,374,379,385,391,396],{"__ignoreMap":264},[268,269,272],"span",{"class":270,"line":271},"line",1,[268,273,274],{},"JobDefinition job = JobDefinition.create(\"Deploy project\")\n",[268,276,278],{"class":270,"line":277},2,[268,279,280],{},"        .name(\"project-deploy\").version(\"1\")\n",[268,282,284],{"class":270,"line":283},3,[268,285,286],{},"        .input(project)                       \u002F\u002F seeds the scope before the first task\n",[268,288,290],{"class":270,"line":289},4,[268,291,292],{},"        .task(Tasks.fromCallable(\"Resolve deploy target\", new Callable\u003CDeployTarget>() {\n",[268,294,296],{"class":270,"line":295},5,[268,297,299],{"emptyLinePlaceholder":298},true,"\n",[268,301,303],{"class":270,"line":302},6,[268,304,305],{},"            @Autowired\n",[268,307,309],{"class":270,"line":308},7,[268,310,311],{},"            private Project project;          \u002F\u002F injected from the job scope\n",[268,313,315],{"class":270,"line":314},8,[268,316,299],{"emptyLinePlaceholder":298},[268,318,320],{"class":270,"line":319},9,[268,321,322],{},"            @Override\n",[268,324,326],{"class":270,"line":325},10,[268,327,328],{},"            public DeployTarget call() {\n",[268,330,332],{"class":270,"line":331},11,[268,333,334],{},"                return resolveTarget(project);\n",[268,336,338],{"class":270,"line":337},12,[268,339,340],{},"            }\n",[268,342,344],{"class":270,"line":343},13,[268,345,346],{},"        }), Store.state(\"deployTarget\"))\n",[268,348,350],{"class":270,"line":349},14,[268,351,352],{},"        .task(Tasks.fromCallable(\"Sync project source\", new Callable\u003CString>() {\n",[268,354,356],{"class":270,"line":355},15,[268,357,299],{"emptyLinePlaceholder":298},[268,359,361],{"class":270,"line":360},16,[268,362,305],{},[268,364,366],{"class":270,"line":365},17,[268,367,368],{},"            private DeployTarget target;      \u002F\u002F stored by the previous task\n",[268,370,372],{"class":270,"line":371},18,[268,373,299],{"emptyLinePlaceholder":298},[268,375,377],{"class":270,"line":376},19,[268,378,322],{},[268,380,382],{"class":270,"line":381},20,[268,383,384],{},"            public String call() {\n",[268,386,388],{"class":270,"line":387},21,[268,389,390],{},"                return syncSource(target);\n",[268,392,394],{"class":270,"line":393},22,[268,395,340],{},[268,397,399],{"class":270,"line":398},23,[268,400,401],{},"        }));\n",[223,403,404,407,408,411,412,415],{},[227,405,406],{},"name"," and ",[227,409,410],{},"version"," identify the job across runs and are required to run it. ",[227,413,414],{},"input(...)"," stores values in the scope as beans, so the first task's dependencies resolve the same way as any other.",[223,417,418,419,422,423,425,426,429,430,433,434,437,438,441,442,445],{},"Definitions nest: ",[227,420,421],{},"jobDefinition(...)"," adds a whole ",[227,424,253],{}," as one task of another. A nested definition declares a ",[227,427,428],{},"JobScope"," — ",[227,431,432],{},"CHILD"," (the default) gives it a scope of its own that is destroyed when it finishes, while ",[227,435,436],{},"PARENT"," lets it store directly into its parent's scope. Passing ",[227,439,440],{},"parallel = true"," to ",[227,443,444],{},"JobDefinition.create"," makes the definition's tasks execute concurrently; the first failure cancels the in-flight siblings.",[447,448,450],"h3",{"id":449},"task-factories","Task factories",[223,452,453,454,457,458,407,461,464],{},"The ",[227,455,456],{},"Tasks"," class adapts plain functional types into tasks. The instance-taking factories inject the given instance's ",[227,459,460],{},"@Autowired",[227,462,463],{},"@Value"," members against the job scope before invoking it:",[466,467,468,475,484,490,496,502],"ul",{},[469,470,471,474],"li",{},[227,472,473],{},"Tasks.fromCallable(description, callable)"," — the result is the callable's return value",[469,476,477,480,481],{},[227,478,479],{},"Tasks.fromSupplier(description, supplier)"," \u002F ",[227,482,483],{},"Tasks.fromRunnable(description, runnable)",[469,485,486,489],{},[227,487,488],{},"Tasks.fromValue(description, value)"," — a constant",[469,491,492,495],{},[227,493,494],{},"Tasks.fromClass(description, callableClass)"," — a new instance is constructed on each execution, with full injection against the scope",[469,497,498,501],{},[227,499,500],{},"Tasks.noop(description)"," — does nothing",[469,503,504,507],{},[227,505,506],{},"Tasks.transformResult(task, transformer)"," — maps another task's result",[447,509,511],{"id":510},"task-classes","Task classes",[223,513,514,515,518,519,522,523,526,527,530],{},"A whole job can be one annotated class, compiled with ",[227,516,517],{},"JobDefinition.fromTasks(...)",". Each ",[227,520,521],{},"@Task"," method is a task, executed in ",[227,524,525],{},"order","; parameters are injected from the scope by type, and the return value is stored back under the method's ",[227,528,529],{},"store"," mode with a name derived from the returned type:",[259,532,534],{"className":261,"code":533,"language":263,"meta":264,"style":264},"public class ProvisioningTasks {\n\n    @Task(order = 1)\n    public ClusterSpec resolveCluster(Project project) { ... }   \u002F\u002F stored as \"clusterSpec\"\n\n    @Task(order = 2, store = StoreType.STATE)\n    public TopicPlan planTopics(ClusterSpec cluster) { ... }     \u002F\u002F durable decision\n\n}\n\nJobDefinition job = JobDefinition.fromTasks(ProvisioningTasks.class)\n                                 .name(\"provision-kafka\").version(\"1\");\n",[227,535,536,541,545,550,555,559,564,569,573,578,582,587],{"__ignoreMap":264},[268,537,538],{"class":270,"line":271},[268,539,540],{},"public class ProvisioningTasks {\n",[268,542,543],{"class":270,"line":277},[268,544,299],{"emptyLinePlaceholder":298},[268,546,547],{"class":270,"line":283},[268,548,549],{},"    @Task(order = 1)\n",[268,551,552],{"class":270,"line":289},[268,553,554],{},"    public ClusterSpec resolveCluster(Project project) { ... }   \u002F\u002F stored as \"clusterSpec\"\n",[268,556,557],{"class":270,"line":295},[268,558,299],{"emptyLinePlaceholder":298},[268,560,561],{"class":270,"line":302},[268,562,563],{},"    @Task(order = 2, store = StoreType.STATE)\n",[268,565,566],{"class":270,"line":308},[268,567,568],{},"    public TopicPlan planTopics(ClusterSpec cluster) { ... }     \u002F\u002F durable decision\n",[268,570,571],{"class":270,"line":314},[268,572,299],{"emptyLinePlaceholder":298},[268,574,575],{"class":270,"line":319},[268,576,577],{},"}\n",[268,579,580],{"class":270,"line":325},[268,581,299],{"emptyLinePlaceholder":298},[268,583,584],{"class":270,"line":331},[268,585,586],{},"JobDefinition job = JobDefinition.fromTasks(ProvisioningTasks.class)\n",[268,588,589],{"class":270,"line":337},[268,590,591],{},"                                 .name(\"provision-kafka\").version(\"1\");\n",[223,593,594],{},"The class is instantiated once per run with constructor arguments resolved against the application context — it is never a Spring bean itself. Compilation fails fast on duplicate orders and on a task consuming a type that only a later task produces.",[219,596,598],{"id":597},"the-job-scope","The job scope",[223,600,601,604,605,607,608,611,612,615,616,619],{},[227,602,603],{},"JobContext"," is the scope tasks share. Values stored by earlier tasks are available to later ones: beans by type through ",[227,606,460],{}," injection or ",[227,609,610],{},"getBean(Class)",", named values through ",[227,613,614],{},"@Value(\"${name}\")"," or ",[227,617,618],{},"getProperty(String)",". Lookups fall through parent scopes down to the application's own beans and configuration, so a task can inject platform services and job-produced values through the same mechanism.",[219,621,623],{"id":622},"storing-results","Storing results",[223,625,626,627,629],{},"How a value behaves on resume and whether watchers of the run may see it are independent choices, declared together on an immutable ",[227,628,242],{},":",[259,631,633],{"className":261,"code":632,"language":263,"meta":264,"style":264},".task(task)                               \u002F\u002F keep nothing\n.task(task, Store.none().wire())          \u002F\u002F keep nothing, publish to watchers\n.task(task, Store.result(\"widget\"))       \u002F\u002F scope value, re-derived on resume\n.task(task, Store.result(\"widget\").wire())\n.task(task, Store.state(\"decision\"))      \u002F\u002F durable, replayed on resume\n.task(task, Store.state(\"decision\").wire())\n",[227,634,635,640,645,650,655,660],{"__ignoreMap":264},[268,636,637],{"class":270,"line":271},[268,638,639],{},".task(task)                               \u002F\u002F keep nothing\n",[268,641,642],{"class":270,"line":277},[268,643,644],{},".task(task, Store.none().wire())          \u002F\u002F keep nothing, publish to watchers\n",[268,646,647],{"class":270,"line":283},[268,648,649],{},".task(task, Store.result(\"widget\"))       \u002F\u002F scope value, re-derived on resume\n",[268,651,652],{"class":270,"line":289},[268,653,654],{},".task(task, Store.result(\"widget\").wire())\n",[268,656,657],{"class":270,"line":295},[268,658,659],{},".task(task, Store.state(\"decision\"))      \u002F\u002F durable, replayed on resume\n",[268,661,662],{"class":270,"line":302},[268,663,664],{},".task(task, Store.state(\"decision\").wire())\n",[223,666,453,667,670,671,629],{},[227,668,669],{},"StoreType"," axis answers ",[672,673,674],"em",{},"what survives a resume",[466,676,677,685,697],{},[469,678,679,684],{},[232,680,681],{},[227,682,683],{},"NONE"," — the value is discarded. On resume a completed task is skipped.",[469,686,687,692,693,696],{},[232,688,689],{},[227,690,691],{},"RESULT"," — the value is stored in the scope but not persisted. On resume the task re-executes to regenerate it — or its declared reload task runs instead: ",[227,694,695],{},"Store.result(\"workload\").reload(reloadTask)"," pairs a task that creates external state with the task that reloads it from its source of truth, so the creation is never repeated.",[469,698,699,704,705,708,709,712,713,716,717,720],{},[232,700,701],{},[227,702,703],{},"STATE"," — the value is stored in the scope and serialized into the run's ",[227,706,707],{},"TaskRecord",". On resume it is replayed from the record without executing. The value must survive a JSON round trip: a plain class or record with concrete field types. Generic values such as ",[227,710,711],{},"List",", ",[227,714,715],{},"Map",", and ",[227,718,719],{},"Optional"," are rejected at completion, because type erasure would make the record unrestorable.",[223,722,453,723,670,726,729,730,733,734,736,737,740,741,743],{},[227,724,725],{},"wire()",[672,727,728],{},"who may see it",": a wired value is serialized as JSON onto the run's ",[227,731,732],{},"TaskCompletedEvent",", so watchers — including remote ones, which never receive live objects — can read it. Publication rides the event stream only — a watcher joining mid-run replays it, and what outlives the run is decided by the ",[227,735,669],{}," alone. Durable state stays private unless it opts in, a result store can publish without being durable, and ",[227,738,739],{},"Store.none().wire()"," publishes a value that exists only for observers. Wire values only have to serialize, never deserialize, so the ",[227,742,703],{}," restrictions on generics do not apply to them.",[223,745,746,747,749,750,752,753,755],{},"Use ",[227,748,703],{}," for decisions and allocations the rest of the run depends on; use ",[227,751,691],{}," for lookups whose source of truth can answer again; add ",[227,754,725],{}," for any value the run's watchers need, such as the workload id a deploy task allocates so the UI can tail that workload's logs mid-run.",[219,757,759],{"id":758},"dynamic-tasks","Dynamic tasks",[223,761,762,763,766,767,769,770,773,774,777,778,780,781,783],{},"Structure can be decided at runtime: a task may return another ",[227,764,765],{},"Task",", or a whole ",[227,768,253],{},", instead of a value. The returned work is discovered, reported to watchers as a ",[227,771,772],{},"TasksDiscoveredEvent"," flagged ",[227,775,776],{},"dynamic"," and carrying the producing task's path, and executed in place under that path. A dynamic ",[227,779,765],{}," inherits the producing task's ",[227,782,242],{},".",[219,785,787],{"id":786},"asynchronous-results-and-vertx","Asynchronous results and Vert.x",[223,789,790,791,794,795,798,799,802],{},"A task may return its value directly, or asynchronously as a ",[227,792,793],{},"CompletionStage",", a Vert.x ",[227,796,797],{},"Future",", or a ",[227,800,801],{},"Publisher"," (whose last emission is the value) — the framework awaits it before storing.",[223,804,805,806,808,809,811],{},"Prefer the Vert.x ",[227,807,797],{},": it is the platform's async type, and every platform repository and service already returns one. Return a ",[227,810,793],{}," only when a library offers nothing else — the Elasticsearch and Azure clients, for instance.",[223,813,814,815,818,819,822],{},"Every run executes on its own Vert.x virtual-thread context, and each parallel child gets its own. Inside a task, ",[227,816,817],{},"Vertx.currentContext()"," resolves, context-locals such as the platform ",[227,820,821],{},"SecurityContext"," participant work, blocking is permitted, and Vert.x futures can be awaited in place:",[259,824,826],{"className":261,"code":825,"language":263,"meta":264,"style":264},"String answer = vertx.timer(20).map(v -> \"ticked\").await();   \u002F\u002F parks the virtual thread\n",[227,827,828],{"__ignoreMap":264},[268,829,830],{"class":270,"line":271},[268,831,825],{},[223,833,834,844,845,848,849,407,852,854],{},[232,835,836,837,615,840,843],{},"Never block on a future with ",[227,838,839],{},"CompletableFuture.get()",[227,841,842],{},"join()"," inside a task body."," Return the asynchronous value and let the framework await it, or await a Vert.x future in place with ",[227,846,847],{},".await()",". Both of those suspend the run's context so its queued work keeps flowing; ",[227,850,851],{},"get()",[227,853,842],{}," park the thread while it still holds the context, which stalls everything queued behind the task:",[259,856,858],{"className":261,"code":857,"language":263,"meta":264,"style":264},"@Task(order = 2)\npublic DeployTarget load(DeployTargetRepository repository, Project project) {\n    return repository.findById(project.getId())\n                     .toCompletionStage().toCompletableFuture().join();   \u002F\u002F deadlocks the run\n}\n\n@Task(order = 2)\npublic Future\u003CDeployTarget> load(DeployTargetRepository repository, Project project) {\n    return repository.findById(project.getId());                          \u002F\u002F the framework awaits it\n}\n\n@Task(order = 2)\npublic DeployTarget load(DeployTargetRepository repository, Project project) {\n    return repository.findById(project.getId()).await();                  \u002F\u002F or await it in place\n}\n",[227,859,860,865,870,875,880,884,888,892,897,902,906,910,914,918,923],{"__ignoreMap":264},[268,861,862],{"class":270,"line":271},[268,863,864],{},"@Task(order = 2)\n",[268,866,867],{"class":270,"line":277},[268,868,869],{},"public DeployTarget load(DeployTargetRepository repository, Project project) {\n",[268,871,872],{"class":270,"line":283},[268,873,874],{},"    return repository.findById(project.getId())\n",[268,876,877],{"class":270,"line":289},[268,878,879],{},"                     .toCompletionStage().toCompletableFuture().join();   \u002F\u002F deadlocks the run\n",[268,881,882],{"class":270,"line":295},[268,883,577],{},[268,885,886],{"class":270,"line":302},[268,887,299],{"emptyLinePlaceholder":298},[268,889,890],{"class":270,"line":308},[268,891,864],{},[268,893,894],{"class":270,"line":314},[268,895,896],{},"public Future\u003CDeployTarget> load(DeployTargetRepository repository, Project project) {\n",[268,898,899],{"class":270,"line":319},[268,900,901],{},"    return repository.findById(project.getId());                          \u002F\u002F the framework awaits it\n",[268,903,904],{"class":270,"line":325},[268,905,577],{},[268,907,908],{"class":270,"line":331},[268,909,299],{"emptyLinePlaceholder":298},[268,911,912],{"class":270,"line":337},[268,913,864],{},[268,915,916],{"class":270,"line":343},[268,917,869],{},[268,919,920],{"class":270,"line":349},[268,921,922],{},"    return repository.findById(project.getId()).await();                  \u002F\u002F or await it in place\n",[268,924,925],{"class":270,"line":355},[268,926,577],{},[223,928,929,930,933,934,936],{},"How bad the stall is depends on where the future came from. A future bound to the run's context — anything a platform repository or service hands back — delivers its completion ",[672,931,932],{},"through"," that context, so ",[227,935,851],{}," waits on a completion queued behind itself and the run hangs permanently. A raw library future completes on its own thread, so the task does eventually proceed, but everything queued on the run's context — record writes, event delivery — waits until it does.",[219,938,940],{"id":939},"running-and-watching","Running and watching",[223,942,943,946],{},[227,944,945],{},"JobService"," executes definitions:",[259,948,950],{"className":261,"code":949,"language":263,"meta":264,"style":264},"JobRunHandle handle = jobService.run(job, JobOwner.ofApplication(orgId, appId, projectId));\n",[227,951,952],{"__ignoreMap":264},[268,953,954],{"class":270,"line":271},[268,955,949],{},[223,957,958,961,962,712,965,968,969,972],{},[227,959,960],{},"JobOwner"," records which tier the run executes on behalf of — ",[227,963,964],{},"system()",[227,966,967],{},"ofOrganization(...)",", or ",[227,970,971],{},"ofApplication(...)"," — and, for an organization or application owner, the project the run served, so runs can be filtered by owner down to one project.",[223,974,975,976,979,980,983,984,987,988,991,992,783],{},"The handle pairs the persistent run's id with the run's event stream. ",[232,977,978],{},"Execution is lazy",": nothing runs until the first subscriber attaches to ",[227,981,982],{},"handle.getEvents()",", the job executes exactly once no matter how many subscribers attach, and every subscriber replays the full event history from the beginning. ",[227,985,986],{},"handle.completion()"," returns a future for the terminal outcome (subscribing, and so starting the run, if nothing else has), and ",[227,989,990],{},"handle.cancel()"," aborts the run, recording it as ",[227,993,994],{},"CANCELLED",[223,996,997,998,1001,1002,1005,1006,1009],{},"The stream emits the sealed ",[227,999,1000],{},"JobRunEvent"," family, each carrying the task's ",[227,1003,1004],{},"taskPath"," — its ",[227,1007,1008],{},"\u002F","-separated position in the run's task tree:",[466,1011,1012,1022,1030,1038,1049],{},[469,1013,1014,1018,1019,1021],{},[232,1015,1016],{},[227,1017,772],{}," — the task tree, at the start of the run and again as dynamic tasks appear; its ",[227,1020,776],{}," flag says which, so watchers can mark the producing task the moment it reveals a subtree",[469,1023,1024,1029],{},[232,1025,1026],{},[227,1027,1028],{},"TaskStartedEvent"," — a task began executing",[469,1031,1032,1037],{},[232,1033,1034],{},[227,1035,1036],{},"TaskProgressEvent"," — a running task reported progress: a percentage and a message",[469,1039,1040,1044,1045,1048],{},[232,1041,1042],{},[227,1043,732],{}," — a task finished, carrying what it stored: the live value for in-process subscribers, and the JSON ",[227,1046,1047],{},"wireValue"," for wired stores",[469,1050,1051,1056],{},[232,1052,1053],{},[227,1054,1055],{},"TaskFailedEvent"," — a task terminated with a failure",[223,1058,1059],{},"The stream completes when the run does, and errors with the run's failure.",[223,1061,1062,1063,1066],{},"A run's live stream exists only in the process executing it. The run records the executing\nnode's id as ",[227,1064,1065],{},"JobRun.nodeId",", so remote watchers route their watch request to that node - in a\ncluster, any node can serve the persisted runs and records, while the live stream comes from\nthe one node named on the run.",[219,1068,1070],{"id":1069},"reporting-progress","Reporting progress",[223,1072,1073,1074,1077],{},"Every job scope carries a ",[227,1075,1076],{},"ProgressReporter",", so a task with known progress injects it like any other dependency and reports as it works:",[259,1079,1081],{"className":261,"code":1080,"language":263,"meta":264,"style":264},"@Task(order = 1)\npublic void pullImage(ProgressReporter progress) {\n    progress.report(30, \"downloading layers\");\n    progress.report(80, \"extracting\");\n}\n",[227,1082,1083,1088,1093,1098,1103],{"__ignoreMap":264},[268,1084,1085],{"class":270,"line":271},[268,1086,1087],{},"@Task(order = 1)\n",[268,1089,1090],{"class":270,"line":277},[268,1091,1092],{},"public void pullImage(ProgressReporter progress) {\n",[268,1094,1095],{"class":270,"line":283},[268,1096,1097],{},"    progress.report(30, \"downloading layers\");\n",[268,1099,1100],{"class":270,"line":289},[268,1101,1102],{},"    progress.report(80, \"extracting\");\n",[268,1104,1105],{"class":270,"line":295},[268,1106,577],{},[223,1108,1109,1110,1112],{},"Each report reaches watchers as a ",[227,1111,1036],{}," attributed to the reporting task. Reports attach to the task executing on the calling thread, so a report made from a thread the task spawned itself is dropped. Progress is stream-only — it is not persisted with the run's records. Run-level progress needs no reports at all: watchers compute it from the discovered task tree and the completion events.",[219,1114,1116],{"id":1115},"resuming-a-run","Resuming a run",[223,1118,250,1119,615,1122,1124,1125,407,1127,1129],{},[227,1120,1121],{},"FAILED",[227,1123,994],{}," run can be resumed with a definition whose ",[227,1126,406],{},[227,1128,410],{}," match the recorded run:",[259,1131,1133],{"className":261,"code":1132,"language":263,"meta":264,"style":264},"JobRunHandle resumed = jobService.resume(originalRunId, buildJob());\n",[227,1134,1135],{"__ignoreMap":264},[268,1136,1137],{"class":270,"line":271},[268,1138,1132],{},[223,1140,1141,1142,1145,1146,1148],{},"The resumed run is a new run — with its own id and records, linked to the original through ",[227,1143,1144],{},"resumedFrom"," and owned by whoever owned the original. Tasks the original run completed are handled by their ",[227,1147,669],{}," as described above: skipped, re-run or reloaded, or replayed. A task that produced dynamic tasks re-executes so the dynamic structure is regenerated; the regenerated tasks then consult the original run's records at their own paths.",[219,1150,1152],{"id":1151},"run-records","Run records",[223,1154,1155,1156,1159,1160,1162,1163,1166,1167,1169,1170,1172,1173,407,1176,1179,1180,1182,1183,1186],{},"Every run persists a ",[227,1157,1158],{},"JobRun"," — name, version, owner, status, timestamps, error, ",[227,1161,1144],{},", and the ",[227,1164,1165],{},"nodeId"," of the executing node — and one ",[227,1168,707],{}," per task: its ",[227,1171,1004],{},", description, status, timestamps, ",[227,1174,1175],{},"storeType",[227,1177,1178],{},"storedName"," (recorded from discovery when the definition names the store, so a watcher can tell the tasks apart before they complete), the serialized ",[227,1181,703],{}," value in ",[227,1184,1185],{},"stateValue",", and the failure message when it failed. The records are the run's durable history: what a dashboard renders after the event stream is gone, and what a resume replays from.",[223,1188,1189,1190,1192,1193,1196,1197,1200,1201,615,1204,1206,1207,1209],{},"The ledger is written ahead of execution. Each transition is acknowledged by the store before the run moves past it: the ",[227,1191,1158],{}," and the ",[227,1194,1195],{},"PENDING"," records of the task tree before the first task starts, a task's ",[227,1198,1199],{},"RUNNING"," record before its body executes, its ",[227,1202,1203],{},"COMPLETED",[227,1205,1121],{}," record before the next task starts, and the terminal records before the event stream terminates — so a subscriber that sees the stream end can read the final ledger immediately. A node that goes down mid-run therefore leaves a ",[227,1208,1199],{}," run whose records name exactly the task that was in flight, rather than losing the run.",[219,1211,1213],{"id":1212},"watching-a-run-remotely","Watching a run remotely",[223,1215,1216,1219,1220,1223,1224,1227,1228,1231,1232,1234],{},[227,1217,1218],{},"JobService.watchRun(jobRunId)"," attaches to a run executing in this process, replaying it from the start; it returns an empty stream for a run that has finished or never started, and subscribing never starts one. ",[227,1221,1222],{},"JobMonitoringService"," publishes that stream, and the records behind it, to remote callers in the management-api zone. Each node publishes its own instance, scoped by its node id: the finders are ",[227,1225,1226],{},"@ScopeOptional",", so any instance answers an unscoped call, while ",[227,1229,1230],{},"watch"," must be invoked with the scope recorded on ",[227,1233,1065],{}," — the node whose process holds the live stream:",[259,1236,1238],{"className":261,"code":1237,"language":263,"meta":264,"style":264},"@Publish\npublic interface JobMonitoringService {\n\n    @Scope\n    String nodeId();\n\n    @ScopeOptional\n    Future\u003CPage\u003CJobRun>> findJobRuns(Pageable pageable);\n\n    @ScopeOptional\n    Future\u003CJobRun> findJobRun(String jobRunId);\n\n    @ScopeOptional\n    Future\u003CPage\u003CTaskRecord>> findTasks(String jobRunId, Pageable pageable);\n\n    Flux\u003CJobRunEvent> watch(String jobRunId);\n\n}\n",[227,1239,1240,1245,1250,1254,1259,1264,1268,1273,1278,1282,1286,1291,1295,1299,1304,1308,1313,1317],{"__ignoreMap":264},[268,1241,1242],{"class":270,"line":271},[268,1243,1244],{},"@Publish\n",[268,1246,1247],{"class":270,"line":277},[268,1248,1249],{},"public interface JobMonitoringService {\n",[268,1251,1252],{"class":270,"line":283},[268,1253,299],{"emptyLinePlaceholder":298},[268,1255,1256],{"class":270,"line":289},[268,1257,1258],{},"    @Scope\n",[268,1260,1261],{"class":270,"line":295},[268,1262,1263],{},"    String nodeId();\n",[268,1265,1266],{"class":270,"line":302},[268,1267,299],{"emptyLinePlaceholder":298},[268,1269,1270],{"class":270,"line":308},[268,1271,1272],{},"    @ScopeOptional\n",[268,1274,1275],{"class":270,"line":314},[268,1276,1277],{},"    Future\u003CPage\u003CJobRun>> findJobRuns(Pageable pageable);\n",[268,1279,1280],{"class":270,"line":319},[268,1281,299],{"emptyLinePlaceholder":298},[268,1283,1284],{"class":270,"line":325},[268,1285,1272],{},[268,1287,1288],{"class":270,"line":331},[268,1289,1290],{},"    Future\u003CJobRun> findJobRun(String jobRunId);\n",[268,1292,1293],{"class":270,"line":337},[268,1294,299],{"emptyLinePlaceholder":298},[268,1296,1297],{"class":270,"line":343},[268,1298,1272],{},[268,1300,1301],{"class":270,"line":349},[268,1302,1303],{},"    Future\u003CPage\u003CTaskRecord>> findTasks(String jobRunId, Pageable pageable);\n",[268,1305,1306],{"class":270,"line":355},[268,1307,299],{"emptyLinePlaceholder":298},[268,1309,1310],{"class":270,"line":360},[268,1311,1312],{},"    Flux\u003CJobRunEvent> watch(String jobRunId);\n",[268,1314,1315],{"class":270,"line":365},[268,1316,299],{"emptyLinePlaceholder":298},[268,1318,1319],{"class":270,"line":371},[268,1320,577],{},[223,1322,1323,1324,1327,1328,1330,1331,1333,1334,1337],{},"Every call is authorized against the run's recorded owner: an organization or application participant sees only the runs its organization owns, a system participant sees every run. Events cross the wire as JSON carrying a ",[227,1325,1326],{},"type"," discriminator, and a ",[227,1329,732],{}," arrives carrying only its ",[227,1332,1047],{}," — the live ",[227,1335,1336],{},"storedValue"," never leaves the executing process:",[259,1339,1343],{"className":1340,"code":1341,"language":1342,"meta":264,"style":264},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"type\": \"taskCompleted\",\n  \"taskPath\": \"0\u002F3\",\n  \"storeType\": \"STATE\",\n  \"storedName\": \"runtimeWorkloadId\",\n  \"wireValue\": \"wl-8f21\"\n}\n","json",[227,1344,1345,1351,1376,1395,1413,1432,1450],{"__ignoreMap":264},[268,1346,1347],{"class":270,"line":271},[268,1348,1350],{"class":1349},"sMK4o","{\n",[268,1352,1353,1356,1359,1362,1364,1367,1371,1373],{"class":270,"line":277},[268,1354,1355],{"class":1349},"  \"",[268,1357,1326],{"class":1358},"spNyl",[268,1360,1361],{"class":1349},"\"",[268,1363,629],{"class":1349},[268,1365,1366],{"class":1349}," \"",[268,1368,1370],{"class":1369},"sfazB","taskCompleted",[268,1372,1361],{"class":1349},[268,1374,1375],{"class":1349},",\n",[268,1377,1378,1380,1382,1384,1386,1388,1391,1393],{"class":270,"line":283},[268,1379,1355],{"class":1349},[268,1381,1004],{"class":1358},[268,1383,1361],{"class":1349},[268,1385,629],{"class":1349},[268,1387,1366],{"class":1349},[268,1389,1390],{"class":1369},"0\u002F3",[268,1392,1361],{"class":1349},[268,1394,1375],{"class":1349},[268,1396,1397,1399,1401,1403,1405,1407,1409,1411],{"class":270,"line":289},[268,1398,1355],{"class":1349},[268,1400,1175],{"class":1358},[268,1402,1361],{"class":1349},[268,1404,629],{"class":1349},[268,1406,1366],{"class":1349},[268,1408,703],{"class":1369},[268,1410,1361],{"class":1349},[268,1412,1375],{"class":1349},[268,1414,1415,1417,1419,1421,1423,1425,1428,1430],{"class":270,"line":295},[268,1416,1355],{"class":1349},[268,1418,1178],{"class":1358},[268,1420,1361],{"class":1349},[268,1422,629],{"class":1349},[268,1424,1366],{"class":1349},[268,1426,1427],{"class":1369},"runtimeWorkloadId",[268,1429,1361],{"class":1349},[268,1431,1375],{"class":1349},[268,1433,1434,1436,1438,1440,1442,1444,1447],{"class":270,"line":302},[268,1435,1355],{"class":1349},[268,1437,1047],{"class":1358},[268,1439,1361],{"class":1349},[268,1441,629],{"class":1349},[268,1443,1366],{"class":1349},[268,1445,1446],{"class":1369},"wl-8f21",[268,1448,1449],{"class":1349},"\"\n",[268,1451,1452],{"class":270,"line":308},[268,1453,577],{"class":1349},[223,1455,1456,1457,407,1460,1463],{},"A run that is no longer live is rendered from ",[227,1458,1459],{},"findJobRun",[227,1461,1462],{},"findTasks"," alone, so a watcher that reaches a finished run sees the same history the stream would have delivered.",[1465,1466,1467],"style",{},"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 .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":264,"searchDepth":277,"depth":277,"links":1469},[1470,1471,1475,1476,1477,1478,1479,1480,1481,1482,1483],{"id":221,"depth":277,"text":25},{"id":246,"depth":277,"text":247,"children":1472},[1473,1474],{"id":449,"depth":283,"text":450},{"id":510,"depth":283,"text":511},{"id":597,"depth":277,"text":598},{"id":622,"depth":277,"text":623},{"id":758,"depth":277,"text":759},{"id":786,"depth":277,"text":787},{"id":939,"depth":277,"text":940},{"id":1069,"depth":277,"text":1070},{"id":1115,"depth":277,"text":1116},{"id":1151,"depth":277,"text":1152},{"id":1212,"depth":277,"text":1213},"The declarative job framework for long-running platform work.","md",null,{},{"icon":208},{"title":205,"description":1484},"n2q5bXjcUVYWQnwkOlisogur4sW_W3tF5mn4-z_4ykM",[1492,1494],{"title":200,"path":201,"stem":202,"description":1493,"icon":203,"children":-1},"Kinotic Resource Identifier specification.",{"title":210,"path":211,"stem":212,"description":1495,"icon":151,"children":-1},"How a project's microservices and UIs are deployed, stored, and served, and how far the build has come.",1788549867460]