[{"data":1,"prerenderedAt":2665},["ShallowReactive",2],{"navigation":3,"\u002Fdocs\u002Fconcepts\u002Fstate-and-thread-safety":314,"\u002Fdocs\u002Fconcepts\u002Fstate-and-thread-safety-surround":2660},[4,38,86,149,176,219,257,273],{"title":5,"path":6,"stem":7,"children":8,"icon":37},"Getting Started","\u002Fdocs\u002Fgetting-started","1.docs\u002F1.getting-started\u002F1.index",[9,12,17,22,27,32],{"title":10,"path":6,"stem":7,"icon":11},"Getting started","i-lucide-flag",{"title":13,"path":14,"stem":15,"icon":16},"Modules overview","\u002Fdocs\u002Fgetting-started\u002Fmodules-overview","1.docs\u002F1.getting-started\u002F2.modules-overview","i-lucide-boxes",{"title":18,"path":19,"stem":20,"icon":21},"Installation","\u002Fdocs\u002Fgetting-started\u002Finstallation","1.docs\u002F1.getting-started\u002F3.installation","i-lucide-download",{"title":23,"path":24,"stem":25,"icon":26},"License configuration","\u002Fdocs\u002Fgetting-started\u002Flicense-configuration","1.docs\u002F1.getting-started\u002F4.license-configuration","i-lucide-key-round",{"title":28,"path":29,"stem":30,"icon":31},"Your first app","\u002Fdocs\u002Fgetting-started\u002Ffirst-app","1.docs\u002F1.getting-started\u002F5.first-app","i-lucide-square-play",{"title":33,"path":34,"stem":35,"icon":36},"Project setup","\u002Fdocs\u002Fgetting-started\u002Fproject-setup","1.docs\u002F1.getting-started\u002F6.project-setup","i-lucide-package",false,{"title":39,"path":40,"stem":41,"children":42,"icon":45},"Core Concepts","\u002Fdocs\u002Fconcepts","1.docs\u002F2.concepts\u002F1.index",[43,46,51,56,61,66,71,76,81],{"title":44,"path":40,"stem":41,"icon":45},"Core concepts","i-lucide-book-open",{"title":47,"path":48,"stem":49,"icon":50},"Architecture","\u002Fdocs\u002Fconcepts\u002Farchitecture","1.docs\u002F2.concepts\u002F2.architecture","i-lucide-layers",{"title":52,"path":53,"stem":54,"icon":55},"Exactly-once semantics","\u002Fdocs\u002Fconcepts\u002Fexactly-once","1.docs\u002F2.concepts\u002F3.exactly-once","i-lucide-shield-check",{"title":57,"path":58,"stem":59,"icon":60},"Lanes and parallelism","\u002Fdocs\u002Fconcepts\u002Flanes-and-parallelism","1.docs\u002F2.concepts\u002F4.lanes-and-parallelism","i-lucide-split",{"title":62,"path":63,"stem":64,"icon":65},"State and thread-safety","\u002Fdocs\u002Fconcepts\u002Fstate-and-thread-safety","1.docs\u002F2.concepts\u002F5.state-and-thread-safety","i-lucide-lock",{"title":67,"path":68,"stem":69,"icon":70},"Event time and watermarks","\u002Fdocs\u002Fconcepts\u002Fevent-time-and-watermarks","1.docs\u002F2.concepts\u002F6.event-time-and-watermarks","i-lucide-clock",{"title":72,"path":73,"stem":74,"icon":75},"The configuration model","\u002Fdocs\u002Fconcepts\u002Fconfiguration-model","1.docs\u002F2.concepts\u002F7.configuration-model","i-lucide-sliders-horizontal",{"title":77,"path":78,"stem":79,"icon":80},"The error-handling model","\u002Fdocs\u002Fconcepts\u002Ferror-handling-model","1.docs\u002F2.concepts\u002F8.error-handling-model","i-lucide-triangle-alert",{"title":82,"path":83,"stem":84,"icon":85},"How StoatFlow differs from Kafka Streams","\u002Fdocs\u002Fconcepts\u002Fhow-stoatflow-differs-from-ks","1.docs\u002F2.concepts\u002F9.how-stoatflow-differs-from-ks","i-lucide-arrow-left-right",{"title":87,"path":88,"stem":89,"children":90,"icon":93},"Building Topologies","\u002Fdocs\u002Fbuilding","1.docs\u002F3.building\u002F1.index",[91,94,99,104,109,114,119,124,129,134,139,144],{"title":92,"path":88,"stem":89,"icon":93},"Building topologies","i-lucide-blocks",{"title":95,"path":96,"stem":97,"icon":98},"Error handling and DLQ","\u002Fdocs\u002Fbuilding\u002Ferror-handling-dlq","1.docs\u002F3.building\u002F10.error-handling-dlq","i-lucide-circle-x",{"title":100,"path":101,"stem":102,"icon":103},"State stores","\u002Fdocs\u002Fbuilding\u002Fstate-stores","1.docs\u002F3.building\u002F11.state-stores","i-lucide-database",{"title":105,"path":106,"stem":107,"icon":108},"Testing topologies","\u002Fdocs\u002Fbuilding\u002Ftesting","1.docs\u002F3.building\u002F12.testing","i-lucide-flask-conical",{"title":110,"path":111,"stem":112,"icon":113},"Sources and sinks","\u002Fdocs\u002Fbuilding\u002Fstreams-builder","1.docs\u002F3.building\u002F2.streams-builder","i-lucide-import",{"title":115,"path":116,"stem":117,"icon":118},"KStream and KTable operations","\u002Fdocs\u002Fbuilding\u002Fkstream-ktable","1.docs\u002F3.building\u002F3.kstream-ktable","i-lucide-waypoints",{"title":120,"path":121,"stem":122,"icon":123},"Aggregations","\u002Fdocs\u002Fbuilding\u002Faggregations","1.docs\u002F3.building\u002F4.aggregations","i-lucide-sigma",{"title":125,"path":126,"stem":127,"icon":128},"Windowing","\u002Fdocs\u002Fbuilding\u002Fwindowing","1.docs\u002F3.building\u002F5.windowing","i-lucide-calendar-clock",{"title":130,"path":131,"stem":132,"icon":133},"Joins","\u002Fdocs\u002Fbuilding\u002Fjoins","1.docs\u002F3.building\u002F6.joins","i-lucide-git-merge",{"title":135,"path":136,"stem":137,"icon":138},"The Processor API","\u002Fdocs\u002Fbuilding\u002Fprocessor-api","1.docs\u002F3.building\u002F7.processor-api","i-lucide-cpu",{"title":140,"path":141,"stem":142,"icon":143},"Scheduled sources","\u002Fdocs\u002Fbuilding\u002Fscheduled-sources","1.docs\u002F3.building\u002F8.scheduled-sources","i-lucide-timer",{"title":145,"path":146,"stem":147,"icon":148},"Serdes and Avro","\u002Fdocs\u002Fbuilding\u002Fserdes","1.docs\u002F3.building\u002F9.serdes","i-lucide-binary",{"title":150,"path":151,"stem":152,"children":153,"icon":75},"Configuration","\u002Fdocs\u002Fconfiguration","1.docs\u002F4.configuration\u002F1.index",[154,156,161,166,171],{"title":155,"path":151,"stem":152,"icon":75},"How configuration works",{"title":157,"path":158,"stem":159,"icon":160},"Engine configuration (:core)","\u002Fdocs\u002Fconfiguration\u002Fcore-config","1.docs\u002F4.configuration\u002F2.core-config","i-lucide-settings-2",{"title":162,"path":163,"stem":164,"icon":165},"Runtime configuration (:runtime)","\u002Fdocs\u002Fconfiguration\u002Fruntime-config","1.docs\u002F4.configuration\u002F3.runtime-config","i-lucide-server-cog",{"title":167,"path":168,"stem":169,"icon":170},"Defaults, adaptivity, and presets","\u002Fdocs\u002Fconfiguration\u002Fdefaults-and-presets","1.docs\u002F4.configuration\u002F4.defaults-and-presets","i-lucide-gauge",{"title":172,"path":173,"stem":174,"icon":175},"Kafka client configuration","\u002Fdocs\u002Fconfiguration\u002Fkafka-client-config","1.docs\u002F4.configuration\u002F5.kafka-client-config","i-lucide-plug",{"title":177,"path":178,"stem":179,"children":180,"icon":183},"Running in Production","\u002Fdocs\u002Fruntime","1.docs\u002F5.runtime\u002F1.index",[181,184,189,194,199,204,209,214],{"title":182,"path":178,"stem":179,"icon":183},"The runtime","i-lucide-server",{"title":185,"path":186,"stem":187,"icon":188},"The REST API","\u002Fdocs\u002Fruntime\u002Frest-api","1.docs\u002F5.runtime\u002F2.rest-api","i-lucide-globe",{"title":190,"path":191,"stem":192,"icon":193},"Health checks","\u002Fdocs\u002Fruntime\u002Fhealth-checks","1.docs\u002F5.runtime\u002F3.health-checks","i-lucide-heart-pulse",{"title":195,"path":196,"stem":197,"icon":198},"Metrics","\u002Fdocs\u002Fruntime\u002Fmetrics","1.docs\u002F5.runtime\u002F4.metrics","i-lucide-activity",{"title":200,"path":201,"stem":202,"icon":203},"Pause and resume","\u002Fdocs\u002Fruntime\u002Fpause-unpause","1.docs\u002F5.runtime\u002F5.pause-unpause","i-lucide-pause",{"title":205,"path":206,"stem":207,"icon":208},"Plugins and lifecycle hooks","\u002Fdocs\u002Fruntime\u002Fplugins","1.docs\u002F5.runtime\u002F6.plugins","i-lucide-puzzle",{"title":210,"path":211,"stem":212,"icon":213},"Docker images","\u002Fdocs\u002Fruntime\u002Fdocker","1.docs\u002F5.runtime\u002F7.docker","i-lucide-container",{"title":215,"path":216,"stem":217,"icon":218},"GraalVM native image","\u002Fdocs\u002Fruntime\u002Fnative-image","1.docs\u002F5.runtime\u002F8.native-image","i-lucide-zap",{"title":220,"path":221,"stem":222,"children":223,"icon":226},"Deploying & Operating","\u002Fdocs\u002Foperating","1.docs\u002F6.operating\u002F1.index",[224,227,232,237,242,247,252],{"title":225,"path":221,"stem":222,"icon":226},"Deploying and operating","i-lucide-life-buoy",{"title":228,"path":229,"stem":230,"icon":231},"Running on Kubernetes","\u002Fdocs\u002Foperating\u002Fkubernetes","1.docs\u002F6.operating\u002F2.kubernetes","i-lucide-ship",{"title":233,"path":234,"stem":235,"icon":236},"High availability","\u002Fdocs\u002Foperating\u002Fhigh-availability","1.docs\u002F6.operating\u002F3.high-availability","i-lucide-copy",{"title":238,"path":239,"stem":240,"icon":241},"Liveness and readiness probes","\u002Fdocs\u002Foperating\u002Fprobes","1.docs\u002F6.operating\u002F4.probes","i-lucide-stethoscope",{"title":243,"path":244,"stem":245,"icon":246},"Observability","\u002Fdocs\u002Foperating\u002Fobservability","1.docs\u002F6.operating\u002F5.observability","i-lucide-telescope",{"title":248,"path":249,"stem":250,"icon":251},"Tuning under load","\u002Fdocs\u002Foperating\u002Ftuning","1.docs\u002F6.operating\u002F6.tuning","i-lucide-sliders",{"title":253,"path":254,"stem":255,"icon":256},"Production checklist","\u002Fdocs\u002Foperating\u002Fproduction-checklist","1.docs\u002F6.operating\u002F7.production-checklist","i-lucide-clipboard-check",{"title":258,"path":259,"stem":260,"children":261,"icon":85},"Migrating from Kafka Streams","\u002Fdocs\u002Fmigration","1.docs\u002F7.migration\u002F1.index",[262,263,268],{"title":258,"path":259,"stem":260,"icon":85},{"title":264,"path":265,"stem":266,"icon":267},"Migration without carrying state","\u002Fdocs\u002Fmigration\u002Fwithout-data-migration","1.docs\u002F7.migration\u002F2.without-data-migration","i-lucide-sparkles",{"title":269,"path":270,"stem":271,"icon":272},"Migration carrying state","\u002Fdocs\u002Fmigration\u002Fwith-data-migration","1.docs\u002F7.migration\u002F3.with-data-migration","i-lucide-database-backup",{"title":274,"path":275,"stem":276,"children":277,"icon":279},"Reference","\u002Fdocs\u002Freference","1.docs\u002F8.reference\u002F1.index",[278,280,285,290,295,300,305,309],{"title":274,"path":275,"stem":276,"icon":279},"i-lucide-list",{"title":281,"path":282,"stem":283,"icon":284},"Configuration reference","\u002Fdocs\u002Freference\u002Fconfiguration-reference","1.docs\u002F8.reference\u002F2.configuration-reference","i-lucide-table",{"title":286,"path":287,"stem":288,"icon":289},"REST API reference","\u002Fdocs\u002Freference\u002Frest-api-reference","1.docs\u002F8.reference\u002F3.rest-api-reference","i-lucide-network",{"title":291,"path":292,"stem":293,"icon":294},"Gradle plugin reference","\u002Fdocs\u002Freference\u002Fgradle-plugin-reference","1.docs\u002F8.reference\u002F4.gradle-plugin-reference","i-lucide-box",{"title":296,"path":297,"stem":298,"icon":299},"Maven reference","\u002Fdocs\u002Freference\u002Fmaven-reference","1.docs\u002F8.reference\u002F5.maven-reference","i-simple-icons-apachemaven",{"title":301,"path":302,"stem":303,"icon":304},"Kafka Streams compatibility matrix","\u002Fdocs\u002Freference\u002Fks-compatibility-matrix","1.docs\u002F8.reference\u002F6.ks-compatibility-matrix","i-lucide-table-2",{"title":306,"path":307,"stem":308,"icon":170},"Metrics reference","\u002Fdocs\u002Freference\u002Fmetrics-reference","1.docs\u002F8.reference\u002F7.metrics-reference",{"title":310,"path":311,"stem":312,"icon":313},"Glossary","\u002Fdocs\u002Freference\u002Fglossary","1.docs\u002F8.reference\u002F8.glossary","i-lucide-book-a",{"id":315,"title":62,"body":316,"description":2653,"extension":2654,"meta":2655,"navigation":2656,"pageType":2657,"path":63,"seo":2658,"stem":64,"__hash__":2659},"docs\u002F1.docs\u002F2.concepts\u002F5.state-and-thread-safety.md",{"type":317,"value":318,"toc":2646},"minimark",[319,328,373,386,391,394,402,422,428,432,442,445,457,460,471,489,495,1384,1388,1406,1409,1423,2424,2449,2459,2469,2473,2487,2501,2600,2613,2617,2642],[320,321,322,323,327],"p",{},"State in StoatFlow is ",[324,325,326],"strong",{},"global",": every state store lives in the single JVM running your topology, and any processing lane can read or write any key. This page explains why that's safe under concurrent access, the one case where you have to coordinate it yourself, and the store types you can choose from.",[329,330,331],"tldr-panel",{},[332,333,334,341,347,367],"ul",{},[335,336,337,340],"li",{},[324,338,339],{},"One process, all state."," No partition-scoped isolation, no inter-instance lookup — any lane can touch any key.",[335,342,343,346],{},[324,344,345],{},"Safe by construction."," Key affinity routes every record for a given key to the same lane, so per-key updates are serial. Different keys run in parallel with no global lock.",[335,348,349,352,353,357,358,362,363,366],{},[324,350,351],{},"One sharp edge."," A custom ",[354,355,356],"code",{},"Processor"," that read-modify-writes ",[359,360,361],"em",{},"multiple keys"," atomically needs the key-lock utility (",[354,364,365],{},"KeyLockManager",") — the only case where ordering doesn't do the work for you.",[335,368,369,372],{},[324,370,371],{},"Five store types",", each RocksDB-backed (persistent) or in-memory: key-value, window, session, versioned, and timer.",[320,374,375,376,379,380,382,383,385],{},"This is the canonical reference for the thread-safety model. For the engine-level picture of lanes, the commit barrier, and durability, see ",[377,378,47],"a",{"href":48},". For the how-to — declaring stores, choosing types, and accessing them from the DSL or a custom ",[354,381,356],{}," — see ",[377,384,100],{"href":101},".",[387,388,390],"h2",{"id":389},"the-global-state-model","The global state model",[320,392,393],{},"In the standard Kafka Streams model, state is partitioned: a task owns a slice of the keyspace, and its state stores hold only the keys for the partitions that task is assigned. Reading a key that lives on another task means an interactive-query round-trip to another instance.",[320,395,396,397,401],{},"StoatFlow has no partitions to scope state to. Because the application runs as ",[377,398,400],{"href":399},"\u002Fdocs\u002Fconcepts\u002Farchitecture#the-single-instance-model","exactly one instance",", every state store holds the entire keyspace, in one process. Three things follow:",[332,403,404,410,416],{},[335,405,406,409],{},[324,407,408],{},"Any lane can read or write any key."," There is no \"this key belongs to that task\" boundary — a lane processing one record can look up a value written by a record on a completely different key.",[335,411,412,415],{},[324,413,414],{},"No replication of the same data across JVMs."," A single process holds all state; there's no copy of the same store living on another node to keep consistent.",[335,417,418,421],{},[324,419,420],{},"No inter-instance lookup protocol."," A value is either in the local store or it doesn't exist yet — there's never a network hop to find it.",[320,423,424,425,385],{},"Durability is unchanged from the Kafka Streams approach: every write produces a Kafka changelog entry, and on restart the runtime rebuilds local state from the changelog. That side of the story — changelog coupling to the commit barrier and parallel restoration — is on ",[377,426,47],{"href":427},"\u002Fdocs\u002Fconcepts\u002Farchitecture#state-stores-and-durability",[387,429,431],{"id":430},"why-concurrent-access-is-safe","Why concurrent access is safe",[320,433,434,435,438,439,441],{},"Global state plus many lanes running in parallel sounds like it should require locking around every store access. It doesn't, and the reason is ",[324,436,437],{},"key affinity"," (see ",[377,440,57],{"href":58},").",[320,443,444],{},"Records are routed to lanes by their key. The same key always lands on the same lane, every time. So for any single key:",[332,446,447,454],{},[335,448,449,450,453],{},"All updates to that key are processed ",[324,451,452],{},"serially",", by one lane, in the order Kafka delivered them.",[335,455,456],{},"There is never a second lane writing the same key concurrently.",[320,458,459],{},"And across keys:",[332,461,462,468],{},[335,463,464,465,385],{},"Different keys live on different lanes and update ",[324,466,467],{},"in parallel",[335,469,470],{},"Because they're different keys, those updates touch different store entries — no contention.",[320,472,473,474,477,478,481,482,485,486,488],{},"The net effect is per-key serialization ",[359,475,476],{},"without"," a global store lock. You get the ordering guarantee you'd expect from single-threaded processing on each key, and the parallelism of many lanes across the keyspace. Reading and writing a state store from inside an operator — ",[354,479,480],{},"count",", ",[354,483,484],{},"aggregate",", a join, or your own ",[354,487,356],{}," — needs no synchronization from you for the common single-key case.",[320,490,491,492,494],{},"Here, a custom ",[354,493,356],{}," reads the current value for the record's key, updates it, and writes it back. Because the record's key pins it to one lane, this read-modify-write is already serial for that key — no locking required:",[496,497,499,863],"code-tabs",{"group":498},"lang",[500,501,507],"pre",{"className":502,"code":503,"filename":504,"language":505,"meta":506,"style":506},"language-kotlin shiki shiki-themes vitesse-light","import io.stoatflow.core.processor.ContextualProcessor\nimport io.stoatflow.core.processor.ProcessorContext\nimport io.stoatflow.core.processor.Record\nimport io.stoatflow.core.state.KeyValueStore\n\nclass RunningSumProcessor(\n    private val storeName: String,\n) : ContextualProcessor\u003CString, Long, String, Long>() {\n    private lateinit var store: KeyValueStore\u003CString, Long>\n\n    override fun init(context: ProcessorContext\u003CString, Long>) {\n        super.init(context)\n        store = context.getStateStore(storeName)\n    }\n\n    override fun process(record: Record\u003CString, Long>) {\n        \u002F\u002F Single-key read-modify-write: safe with no locking,\n        \u002F\u002F because this key is always handled by the same lane.\n        val current = store.get(record.key()) ?: 0L\n        val updated = current + record.value()\n        store.put(record.key(), updated)\n        context().forward(record.withValue(updated))\n    }\n}\n","Kotlin","kotlin","",[354,508,509,522,530,538,546,553,567,586,616,644,649,678,693,712,718,723,749,756,762,792,816,832,852,857],{"__ignoreMap":506},[510,511,514,518],"span",{"class":512,"line":513},"line",1,[510,515,517],{"class":516},"sbBg2","import",[510,519,521],{"class":520},"sySUi"," io.stoatflow.core.processor.ContextualProcessor\n",[510,523,525,527],{"class":512,"line":524},2,[510,526,517],{"class":516},[510,528,529],{"class":520}," io.stoatflow.core.processor.ProcessorContext\n",[510,531,533,535],{"class":512,"line":532},3,[510,534,517],{"class":516},[510,536,537],{"class":520}," io.stoatflow.core.processor.Record\n",[510,539,541,543],{"class":512,"line":540},4,[510,542,517],{"class":516},[510,544,545],{"class":520}," io.stoatflow.core.state.KeyValueStore\n",[510,547,549],{"class":512,"line":548},5,[510,550,552],{"emptyLinePlaceholder":551},true,"\n",[510,554,556,559,563],{"class":512,"line":555},6,[510,557,558],{"class":516},"class",[510,560,562],{"class":561},"sUxyF"," RunningSumProcessor",[510,564,566],{"class":565},"suHK_","(\n",[510,568,570,574,577,580,583],{"class":512,"line":569},7,[510,571,573],{"class":572},"si04Y","    private",[510,575,576],{"class":516}," val",[510,578,579],{"class":565}," storeName: ",[510,581,582],{"class":561},"String",[510,584,585],{"class":565},",\n",[510,587,589,592,595,598,600,602,605,607,609,611,613],{"class":512,"line":588},8,[510,590,591],{"class":565},") : ",[510,593,594],{"class":561},"ContextualProcessor",[510,596,597],{"class":565},"\u003C",[510,599,582],{"class":561},[510,601,481],{"class":565},[510,603,604],{"class":561},"Long",[510,606,481],{"class":565},[510,608,582],{"class":561},[510,610,481],{"class":565},[510,612,604],{"class":561},[510,614,615],{"class":565},">() {\n",[510,617,619,621,624,627,630,633,635,637,639,641],{"class":512,"line":618},9,[510,620,573],{"class":572},[510,622,623],{"class":572}," lateinit",[510,625,626],{"class":516}," var",[510,628,629],{"class":565}," store: ",[510,631,632],{"class":561},"KeyValueStore",[510,634,597],{"class":565},[510,636,582],{"class":561},[510,638,481],{"class":565},[510,640,604],{"class":561},[510,642,643],{"class":565},">\n",[510,645,647],{"class":512,"line":646},10,[510,648,552],{"emptyLinePlaceholder":551},[510,650,652,655,658,661,664,667,669,671,673,675],{"class":512,"line":651},11,[510,653,654],{"class":572},"    override",[510,656,657],{"class":516}," fun",[510,659,660],{"class":520}," init",[510,662,663],{"class":565},"(context: ",[510,665,666],{"class":561},"ProcessorContext",[510,668,597],{"class":565},[510,670,582],{"class":561},[510,672,481],{"class":565},[510,674,604],{"class":561},[510,676,677],{"class":565},">) {\n",[510,679,681,685,687,690],{"class":512,"line":680},12,[510,682,684],{"class":683},"sEi1f","        super",[510,686,385],{"class":565},[510,688,689],{"class":520},"init",[510,691,692],{"class":565},"(context)\n",[510,694,696,699,703,706,709],{"class":512,"line":695},13,[510,697,698],{"class":565},"        store ",[510,700,702],{"class":701},"sYZai","=",[510,704,705],{"class":565}," context.",[510,707,708],{"class":520},"getStateStore",[510,710,711],{"class":565},"(storeName)\n",[510,713,715],{"class":512,"line":714},14,[510,716,717],{"class":565},"    }\n",[510,719,721],{"class":512,"line":720},15,[510,722,552],{"emptyLinePlaceholder":551},[510,724,726,728,730,733,736,739,741,743,745,747],{"class":512,"line":725},16,[510,727,654],{"class":572},[510,729,657],{"class":516},[510,731,732],{"class":520}," process",[510,734,735],{"class":565},"(record: ",[510,737,738],{"class":561},"Record",[510,740,597],{"class":565},[510,742,582],{"class":561},[510,744,481],{"class":565},[510,746,604],{"class":561},[510,748,677],{"class":565},[510,750,752],{"class":512,"line":751},17,[510,753,755],{"class":754},"s8zF2","        \u002F\u002F Single-key read-modify-write: safe with no locking,\n",[510,757,759],{"class":512,"line":758},18,[510,760,761],{"class":754},"        \u002F\u002F because this key is always handled by the same lane.\n",[510,763,765,768,771,773,776,779,782,785,788],{"class":512,"line":764},19,[510,766,767],{"class":516},"        val",[510,769,770],{"class":565}," current ",[510,772,702],{"class":701},[510,774,775],{"class":565}," store.",[510,777,778],{"class":520},"get",[510,780,781],{"class":565},"(record.",[510,783,784],{"class":520},"key",[510,786,787],{"class":565},"()) ?: ",[510,789,791],{"class":790},"s-TwI","0L\n",[510,793,795,797,800,802,804,807,810,813],{"class":512,"line":794},20,[510,796,767],{"class":516},[510,798,799],{"class":565}," updated ",[510,801,702],{"class":701},[510,803,770],{"class":565},[510,805,806],{"class":572},"+",[510,808,809],{"class":565}," record.",[510,811,812],{"class":520},"value",[510,814,815],{"class":565},"()\n",[510,817,819,822,825,827,829],{"class":512,"line":818},21,[510,820,821],{"class":565},"        store.",[510,823,824],{"class":520},"put",[510,826,781],{"class":565},[510,828,784],{"class":520},[510,830,831],{"class":565},"(), updated)\n",[510,833,835,838,841,844,846,849],{"class":512,"line":834},22,[510,836,837],{"class":520},"        context",[510,839,840],{"class":565},"().",[510,842,843],{"class":520},"forward",[510,845,781],{"class":565},[510,847,848],{"class":520},"withValue",[510,850,851],{"class":565},"(updated))\n",[510,853,855],{"class":512,"line":854},23,[510,856,717],{"class":565},[510,858,860],{"class":512,"line":859},24,[510,861,862],{"class":565},"}\n",[500,864,869],{"className":865,"code":866,"filename":867,"language":868,"meta":506,"style":506},"language-java shiki shiki-themes vitesse-light","import io.stoatflow.core.processor.ContextualProcessor;\nimport io.stoatflow.core.processor.ProcessorContext;\nimport io.stoatflow.core.processor.Record;\nimport io.stoatflow.core.state.KeyValueStore;\n\npublic class RunningSumProcessor\n        extends ContextualProcessor\u003CString, Long, String, Long> {\n\n    private final String storeName;\n    private KeyValueStore\u003CString, Long> store;\n\n    public RunningSumProcessor(String storeName) {\n        this.storeName = storeName;\n    }\n\n    @Override\n    public void init(ProcessorContext\u003CString, Long> context) {\n        super.init(context);\n        this.store = context.getStateStore(storeName);\n    }\n\n    @Override\n    public void process(Record\u003CString, Long> record) {\n        \u002F\u002F Single-key read-modify-write: safe with no locking,\n        \u002F\u002F because this key is always handled by the same lane.\n        long current = store.get(record.key()) == null ? 0L : store.get(record.key());\n        long updated = current + record.value();\n        store.put(record.key(), updated);\n        context().forward(record.withValue(updated));\n    }\n}\n","Java","java",[354,870,871,900,924,948,973,977,988,1021,1025,1040,1062,1066,1087,1103,1107,1111,1119,1149,1165,1188,1192,1196,1202,1231,1235,1240,1301,1324,1349,1374,1379],{"__ignoreMap":506},[510,872,873,875,878,880,883,885,888,890,893,895,897],{"class":512,"line":513},[510,874,517],{"class":516},[510,876,877],{"class":565}," io",[510,879,385],{"class":701},[510,881,882],{"class":565},"stoatflow",[510,884,385],{"class":701},[510,886,887],{"class":565},"core",[510,889,385],{"class":701},[510,891,892],{"class":565},"processor",[510,894,385],{"class":701},[510,896,594],{"class":565},[510,898,899],{"class":701},";\n",[510,901,902,904,906,908,910,912,914,916,918,920,922],{"class":512,"line":524},[510,903,517],{"class":516},[510,905,877],{"class":565},[510,907,385],{"class":701},[510,909,882],{"class":565},[510,911,385],{"class":701},[510,913,887],{"class":565},[510,915,385],{"class":701},[510,917,892],{"class":565},[510,919,385],{"class":701},[510,921,666],{"class":565},[510,923,899],{"class":701},[510,925,926,928,930,932,934,936,938,940,942,944,946],{"class":512,"line":532},[510,927,517],{"class":516},[510,929,877],{"class":565},[510,931,385],{"class":701},[510,933,882],{"class":565},[510,935,385],{"class":701},[510,937,887],{"class":565},[510,939,385],{"class":701},[510,941,892],{"class":565},[510,943,385],{"class":701},[510,945,738],{"class":565},[510,947,899],{"class":701},[510,949,950,952,954,956,958,960,962,964,967,969,971],{"class":512,"line":540},[510,951,517],{"class":516},[510,953,877],{"class":565},[510,955,385],{"class":701},[510,957,882],{"class":565},[510,959,385],{"class":701},[510,961,887],{"class":565},[510,963,385],{"class":701},[510,965,966],{"class":565},"state",[510,968,385],{"class":701},[510,970,632],{"class":565},[510,972,899],{"class":701},[510,974,975],{"class":512,"line":548},[510,976,552],{"emptyLinePlaceholder":551},[510,978,979,982,985],{"class":512,"line":555},[510,980,981],{"class":572},"public",[510,983,984],{"class":572}," class",[510,986,987],{"class":561}," RunningSumProcessor\n",[510,989,990,993,996,998,1000,1003,1006,1008,1011,1013,1015,1018],{"class":512,"line":569},[510,991,992],{"class":572},"        extends",[510,994,995],{"class":520}," ContextualProcessor",[510,997,597],{"class":701},[510,999,582],{"class":572},[510,1001,1002],{"class":701},",",[510,1004,1005],{"class":572}," Long",[510,1007,1002],{"class":701},[510,1009,1010],{"class":572}," String",[510,1012,1002],{"class":701},[510,1014,1005],{"class":572},[510,1016,1017],{"class":701},">",[510,1019,1020],{"class":701}," {\n",[510,1022,1023],{"class":512,"line":588},[510,1024,552],{"emptyLinePlaceholder":551},[510,1026,1027,1029,1032,1034,1038],{"class":512,"line":618},[510,1028,573],{"class":572},[510,1030,1031],{"class":572}," final",[510,1033,1010],{"class":565},[510,1035,1037],{"class":1036},"svycV"," storeName",[510,1039,899],{"class":701},[510,1041,1042,1044,1047,1049,1051,1053,1055,1057,1060],{"class":512,"line":646},[510,1043,573],{"class":572},[510,1045,1046],{"class":565}," KeyValueStore",[510,1048,597],{"class":701},[510,1050,582],{"class":572},[510,1052,1002],{"class":701},[510,1054,1005],{"class":572},[510,1056,1017],{"class":701},[510,1058,1059],{"class":1036}," store",[510,1061,899],{"class":701},[510,1063,1064],{"class":512,"line":651},[510,1065,552],{"emptyLinePlaceholder":551},[510,1067,1068,1071,1073,1076,1079,1082,1085],{"class":512,"line":680},[510,1069,1070],{"class":572},"    public",[510,1072,562],{"class":520},[510,1074,1075],{"class":701},"(",[510,1077,1078],{"class":565},"String ",[510,1080,1081],{"class":1036},"storeName",[510,1083,1084],{"class":701},")",[510,1086,1020],{"class":701},[510,1088,1089,1092,1094,1096,1099,1101],{"class":512,"line":695},[510,1090,1091],{"class":683},"        this",[510,1093,385],{"class":701},[510,1095,1081],{"class":1036},[510,1097,1098],{"class":701}," =",[510,1100,1037],{"class":565},[510,1102,899],{"class":701},[510,1104,1105],{"class":512,"line":714},[510,1106,717],{"class":701},[510,1108,1109],{"class":512,"line":720},[510,1110,552],{"emptyLinePlaceholder":551},[510,1112,1113,1116],{"class":512,"line":725},[510,1114,1115],{"class":701},"    @",[510,1117,1118],{"class":572},"Override\n",[510,1120,1121,1123,1126,1128,1130,1132,1134,1136,1138,1140,1142,1145,1147],{"class":512,"line":751},[510,1122,1070],{"class":572},[510,1124,1125],{"class":572}," void",[510,1127,660],{"class":520},[510,1129,1075],{"class":701},[510,1131,666],{"class":565},[510,1133,597],{"class":701},[510,1135,582],{"class":572},[510,1137,1002],{"class":701},[510,1139,1005],{"class":572},[510,1141,1017],{"class":701},[510,1143,1144],{"class":1036}," context",[510,1146,1084],{"class":701},[510,1148,1020],{"class":701},[510,1150,1151,1153,1155,1157,1159,1162],{"class":512,"line":758},[510,1152,684],{"class":683},[510,1154,385],{"class":701},[510,1156,689],{"class":520},[510,1158,1075],{"class":701},[510,1160,1161],{"class":565},"context",[510,1163,1164],{"class":701},");\n",[510,1166,1167,1169,1171,1174,1176,1178,1180,1182,1184,1186],{"class":512,"line":764},[510,1168,1091],{"class":683},[510,1170,385],{"class":701},[510,1172,1173],{"class":1036},"store",[510,1175,1098],{"class":701},[510,1177,1144],{"class":1036},[510,1179,385],{"class":701},[510,1181,708],{"class":520},[510,1183,1075],{"class":701},[510,1185,1081],{"class":565},[510,1187,1164],{"class":701},[510,1189,1190],{"class":512,"line":794},[510,1191,717],{"class":701},[510,1193,1194],{"class":512,"line":818},[510,1195,552],{"emptyLinePlaceholder":551},[510,1197,1198,1200],{"class":512,"line":834},[510,1199,1115],{"class":701},[510,1201,1118],{"class":572},[510,1203,1204,1206,1208,1210,1212,1214,1216,1218,1220,1222,1224,1227,1229],{"class":512,"line":854},[510,1205,1070],{"class":572},[510,1207,1125],{"class":572},[510,1209,732],{"class":520},[510,1211,1075],{"class":701},[510,1213,738],{"class":565},[510,1215,597],{"class":701},[510,1217,582],{"class":572},[510,1219,1002],{"class":701},[510,1221,1005],{"class":572},[510,1223,1017],{"class":701},[510,1225,1226],{"class":1036}," record",[510,1228,1084],{"class":701},[510,1230,1020],{"class":701},[510,1232,1233],{"class":512,"line":859},[510,1234,755],{"class":754},[510,1236,1238],{"class":512,"line":1237},25,[510,1239,761],{"class":754},[510,1241,1243,1246,1249,1251,1253,1255,1257,1259,1262,1264,1266,1269,1272,1275,1278,1281,1284,1286,1288,1290,1292,1294,1296,1298],{"class":512,"line":1242},26,[510,1244,1245],{"class":572},"        long",[510,1247,1248],{"class":1036}," current",[510,1250,1098],{"class":701},[510,1252,1059],{"class":1036},[510,1254,385],{"class":701},[510,1256,778],{"class":520},[510,1258,1075],{"class":701},[510,1260,1261],{"class":1036},"record",[510,1263,385],{"class":701},[510,1265,784],{"class":520},[510,1267,1268],{"class":701},"())",[510,1270,1271],{"class":572}," ==",[510,1273,1274],{"class":516}," null",[510,1276,1277],{"class":516}," ?",[510,1279,1280],{"class":790}," 0L",[510,1282,1283],{"class":516}," :",[510,1285,1059],{"class":1036},[510,1287,385],{"class":701},[510,1289,778],{"class":520},[510,1291,1075],{"class":701},[510,1293,1261],{"class":1036},[510,1295,385],{"class":701},[510,1297,784],{"class":520},[510,1299,1300],{"class":701},"());\n",[510,1302,1304,1306,1309,1311,1313,1315,1317,1319,1321],{"class":512,"line":1303},27,[510,1305,1245],{"class":572},[510,1307,1308],{"class":1036}," updated",[510,1310,1098],{"class":701},[510,1312,770],{"class":565},[510,1314,806],{"class":572},[510,1316,1226],{"class":1036},[510,1318,385],{"class":701},[510,1320,812],{"class":520},[510,1322,1323],{"class":701},"();\n",[510,1325,1327,1330,1332,1334,1336,1338,1340,1342,1345,1347],{"class":512,"line":1326},28,[510,1328,1329],{"class":1036},"        store",[510,1331,385],{"class":701},[510,1333,824],{"class":520},[510,1335,1075],{"class":701},[510,1337,1261],{"class":1036},[510,1339,385],{"class":701},[510,1341,784],{"class":520},[510,1343,1344],{"class":701},"(),",[510,1346,1308],{"class":565},[510,1348,1164],{"class":701},[510,1350,1352,1354,1356,1358,1360,1362,1364,1366,1368,1371],{"class":512,"line":1351},29,[510,1353,837],{"class":520},[510,1355,840],{"class":701},[510,1357,843],{"class":520},[510,1359,1075],{"class":701},[510,1361,1261],{"class":1036},[510,1363,385],{"class":701},[510,1365,848],{"class":520},[510,1367,1075],{"class":701},[510,1369,1370],{"class":565},"updated",[510,1372,1373],{"class":701},"));\n",[510,1375,1377],{"class":512,"line":1376},30,[510,1378,717],{"class":701},[510,1380,1382],{"class":512,"line":1381},31,[510,1383,862],{"class":701},[387,1385,1387],{"id":1386},"the-one-case-that-needs-coordination-cross-key-atomic-updates","The one case that needs coordination: cross-key atomic updates",[320,1389,1390,1391,1394,1395,1398,1399,1401,1402,1405],{},"Key affinity serializes access ",[359,1392,1393],{},"per key",". It says nothing about two ",[324,1396,1397],{},"different"," keys handled by two different lanes at the same time. That's almost always fine — different keys are independent. But there's one pattern it doesn't cover: a custom ",[354,1400,356],{}," that needs to read-modify-write ",[324,1403,1404],{},"more than one key as a single atomic step",", where another lane might be touching one of those same keys concurrently.",[320,1407,1408],{},"Examples: moving a balance from account A to account B (debit one, credit the other, with no moment where the total is wrong), or maintaining an invariant that spans a pair of related keys. Because A and B may hash to different lanes, two records could enter the critical section at the same time, and the interleaving would corrupt the invariant.",[320,1410,1411,1412,1415,1416,1418,1419,1422],{},"For this — and ",[324,1413,1414],{},"only"," this — StoatFlow provides a key-lock utility, ",[354,1417,365],{}," (",[354,1420,1421],{},"io.stoatflow.core.state.KeyLockManager","). You instantiate and own it; the runtime never creates one for you. It guards a short critical section across one or more keys, acquiring locks in a deterministic order so concurrent multi-key sections can't deadlock against each other.",[496,1424,1425,1760],{"group":498},[500,1426,1428],{"className":502,"code":1427,"filename":504,"language":505,"meta":506,"style":506},"import io.stoatflow.core.processor.ContextualProcessor\nimport io.stoatflow.core.processor.ProcessorContext\nimport io.stoatflow.core.processor.Record\nimport io.stoatflow.core.state.KeyLockManager\nimport io.stoatflow.core.state.KeyValueStore\n\nclass TransferProcessor(\n    private val storeName: String,\n) : ContextualProcessor\u003CString, Transfer, String, Transfer>() {\n    private lateinit var balances: KeyValueStore\u003CString, Long>\n\n    \u002F\u002F One instance per store; share one across stores for cross-store atomicity.\n    private val keyLocks = KeyLockManager()\n\n    override fun init(context: ProcessorContext\u003CString, Transfer>) {\n        super.init(context)\n        balances = context.getStateStore(storeName)\n    }\n\n    override fun process(record: Record\u003CString, Transfer>) {\n        val t = record.value()\n        \u002F\u002F Both keys may live on different lanes — lock both for the atomic move.\n        keyLocks.withLocks(t.from, t.to) {\n            val fromBalance = balances.get(t.from) ?: 0L\n            val toBalance = balances.get(t.to) ?: 0L\n            balances.put(t.from, fromBalance - t.amount)\n            balances.put(t.to, toBalance + t.amount)\n        }\n        context().forward(record)\n    }\n}\n",[354,1429,1430,1436,1442,1448,1455,1461,1465,1474,1486,1511,1534,1538,1543,1559,1563,1585,1595,1608,1612,1616,1638,1653,1658,1669,1689,1707,1723,1736,1741,1752,1756],{"__ignoreMap":506},[510,1431,1432,1434],{"class":512,"line":513},[510,1433,517],{"class":516},[510,1435,521],{"class":520},[510,1437,1438,1440],{"class":512,"line":524},[510,1439,517],{"class":516},[510,1441,529],{"class":520},[510,1443,1444,1446],{"class":512,"line":532},[510,1445,517],{"class":516},[510,1447,537],{"class":520},[510,1449,1450,1452],{"class":512,"line":540},[510,1451,517],{"class":516},[510,1453,1454],{"class":520}," io.stoatflow.core.state.KeyLockManager\n",[510,1456,1457,1459],{"class":512,"line":548},[510,1458,517],{"class":516},[510,1460,545],{"class":520},[510,1462,1463],{"class":512,"line":555},[510,1464,552],{"emptyLinePlaceholder":551},[510,1466,1467,1469,1472],{"class":512,"line":569},[510,1468,558],{"class":516},[510,1470,1471],{"class":561}," TransferProcessor",[510,1473,566],{"class":565},[510,1475,1476,1478,1480,1482,1484],{"class":512,"line":588},[510,1477,573],{"class":572},[510,1479,576],{"class":516},[510,1481,579],{"class":565},[510,1483,582],{"class":561},[510,1485,585],{"class":565},[510,1487,1488,1490,1492,1494,1496,1498,1501,1503,1505,1507,1509],{"class":512,"line":618},[510,1489,591],{"class":565},[510,1491,594],{"class":561},[510,1493,597],{"class":565},[510,1495,582],{"class":561},[510,1497,481],{"class":565},[510,1499,1500],{"class":561},"Transfer",[510,1502,481],{"class":565},[510,1504,582],{"class":561},[510,1506,481],{"class":565},[510,1508,1500],{"class":561},[510,1510,615],{"class":565},[510,1512,1513,1515,1517,1519,1522,1524,1526,1528,1530,1532],{"class":512,"line":646},[510,1514,573],{"class":572},[510,1516,623],{"class":572},[510,1518,626],{"class":516},[510,1520,1521],{"class":565}," balances: ",[510,1523,632],{"class":561},[510,1525,597],{"class":565},[510,1527,582],{"class":561},[510,1529,481],{"class":565},[510,1531,604],{"class":561},[510,1533,643],{"class":565},[510,1535,1536],{"class":512,"line":651},[510,1537,552],{"emptyLinePlaceholder":551},[510,1539,1540],{"class":512,"line":680},[510,1541,1542],{"class":754},"    \u002F\u002F One instance per store; share one across stores for cross-store atomicity.\n",[510,1544,1545,1547,1549,1552,1554,1557],{"class":512,"line":695},[510,1546,573],{"class":572},[510,1548,576],{"class":516},[510,1550,1551],{"class":565}," keyLocks ",[510,1553,702],{"class":701},[510,1555,1556],{"class":520}," KeyLockManager",[510,1558,815],{"class":565},[510,1560,1561],{"class":512,"line":714},[510,1562,552],{"emptyLinePlaceholder":551},[510,1564,1565,1567,1569,1571,1573,1575,1577,1579,1581,1583],{"class":512,"line":720},[510,1566,654],{"class":572},[510,1568,657],{"class":516},[510,1570,660],{"class":520},[510,1572,663],{"class":565},[510,1574,666],{"class":561},[510,1576,597],{"class":565},[510,1578,582],{"class":561},[510,1580,481],{"class":565},[510,1582,1500],{"class":561},[510,1584,677],{"class":565},[510,1586,1587,1589,1591,1593],{"class":512,"line":725},[510,1588,684],{"class":683},[510,1590,385],{"class":565},[510,1592,689],{"class":520},[510,1594,692],{"class":565},[510,1596,1597,1600,1602,1604,1606],{"class":512,"line":751},[510,1598,1599],{"class":565},"        balances ",[510,1601,702],{"class":701},[510,1603,705],{"class":565},[510,1605,708],{"class":520},[510,1607,711],{"class":565},[510,1609,1610],{"class":512,"line":758},[510,1611,717],{"class":565},[510,1613,1614],{"class":512,"line":764},[510,1615,552],{"emptyLinePlaceholder":551},[510,1617,1618,1620,1622,1624,1626,1628,1630,1632,1634,1636],{"class":512,"line":794},[510,1619,654],{"class":572},[510,1621,657],{"class":516},[510,1623,732],{"class":520},[510,1625,735],{"class":565},[510,1627,738],{"class":561},[510,1629,597],{"class":565},[510,1631,582],{"class":561},[510,1633,481],{"class":565},[510,1635,1500],{"class":561},[510,1637,677],{"class":565},[510,1639,1640,1642,1645,1647,1649,1651],{"class":512,"line":818},[510,1641,767],{"class":516},[510,1643,1644],{"class":565}," t ",[510,1646,702],{"class":701},[510,1648,809],{"class":565},[510,1650,812],{"class":520},[510,1652,815],{"class":565},[510,1654,1655],{"class":512,"line":834},[510,1656,1657],{"class":754},"        \u002F\u002F Both keys may live on different lanes — lock both for the atomic move.\n",[510,1659,1660,1663,1666],{"class":512,"line":854},[510,1661,1662],{"class":565},"        keyLocks.",[510,1664,1665],{"class":520},"withLocks",[510,1667,1668],{"class":565},"(t.from, t.to) {\n",[510,1670,1671,1674,1677,1679,1682,1684,1687],{"class":512,"line":859},[510,1672,1673],{"class":516},"            val",[510,1675,1676],{"class":565}," fromBalance ",[510,1678,702],{"class":701},[510,1680,1681],{"class":565}," balances.",[510,1683,778],{"class":520},[510,1685,1686],{"class":565},"(t.from) ?: ",[510,1688,791],{"class":790},[510,1690,1691,1693,1696,1698,1700,1702,1705],{"class":512,"line":1237},[510,1692,1673],{"class":516},[510,1694,1695],{"class":565}," toBalance ",[510,1697,702],{"class":701},[510,1699,1681],{"class":565},[510,1701,778],{"class":520},[510,1703,1704],{"class":565},"(t.to) ?: ",[510,1706,791],{"class":790},[510,1708,1709,1712,1714,1717,1720],{"class":512,"line":1242},[510,1710,1711],{"class":565},"            balances.",[510,1713,824],{"class":520},[510,1715,1716],{"class":565},"(t.from, fromBalance ",[510,1718,1719],{"class":572},"-",[510,1721,1722],{"class":565}," t.amount)\n",[510,1724,1725,1727,1729,1732,1734],{"class":512,"line":1303},[510,1726,1711],{"class":565},[510,1728,824],{"class":520},[510,1730,1731],{"class":565},"(t.to, toBalance ",[510,1733,806],{"class":572},[510,1735,1722],{"class":565},[510,1737,1738],{"class":512,"line":1326},[510,1739,1740],{"class":565},"        }\n",[510,1742,1743,1745,1747,1749],{"class":512,"line":1351},[510,1744,837],{"class":520},[510,1746,840],{"class":565},[510,1748,843],{"class":520},[510,1750,1751],{"class":565},"(record)\n",[510,1753,1754],{"class":512,"line":1376},[510,1755,717],{"class":565},[510,1757,1758],{"class":512,"line":1381},[510,1759,862],{"class":565},[500,1761,1763],{"className":865,"code":1762,"filename":867,"language":868,"meta":506,"style":506},"import io.stoatflow.core.processor.ContextualProcessor;\nimport io.stoatflow.core.processor.ProcessorContext;\nimport io.stoatflow.core.processor.Record;\nimport io.stoatflow.core.state.KeyLockManager;\nimport io.stoatflow.core.state.KeyValueStore;\n\npublic class TransferProcessor\n        extends ContextualProcessor\u003CString, Transfer, String, Transfer> {\n\n    private final String storeName;\n    private KeyValueStore\u003CString, Long> balances;\n\n    \u002F\u002F One instance per store; share one across stores for cross-store atomicity.\n    private final KeyLockManager keyLocks = new KeyLockManager();\n\n    public TransferProcessor(String storeName) {\n        this.storeName = storeName;\n    }\n\n    @Override\n    public void init(ProcessorContext\u003CString, Transfer> context) {\n        super.init(context);\n        this.balances = context.getStateStore(storeName);\n    }\n\n    @Override\n    public void process(Record\u003CString, Transfer> record) {\n        Transfer t = record.value();\n        \u002F\u002F Both keys may live on different lanes — lock both for the atomic move.\n        keyLocks.withLocks(new Object[]{ t.from, t.to }, () -> {\n            long fromBalance = balances.get(t.from) == null ? 0L : balances.get(t.from);\n            long toBalance = balances.get(t.to) == null ? 0L : balances.get(t.to);\n            balances.put(t.from, fromBalance - t.amount);\n            balances.put(t.to, toBalance + t.amount);\n            return null;\n        });\n        context().forward(record);\n    }\n}\n",[354,1764,1765,1789,1813,1837,1861,1885,1889,1898,1925,1929,1941,1962,1966,1970,1990,1994,2010,2024,2028,2032,2038,2066,2080,2103,2107,2111,2117,2145,2163,2167,2214,2267,2319,2352,2383,2393,2399,2414,2419],{"__ignoreMap":506},[510,1766,1767,1769,1771,1773,1775,1777,1779,1781,1783,1785,1787],{"class":512,"line":513},[510,1768,517],{"class":516},[510,1770,877],{"class":565},[510,1772,385],{"class":701},[510,1774,882],{"class":565},[510,1776,385],{"class":701},[510,1778,887],{"class":565},[510,1780,385],{"class":701},[510,1782,892],{"class":565},[510,1784,385],{"class":701},[510,1786,594],{"class":565},[510,1788,899],{"class":701},[510,1790,1791,1793,1795,1797,1799,1801,1803,1805,1807,1809,1811],{"class":512,"line":524},[510,1792,517],{"class":516},[510,1794,877],{"class":565},[510,1796,385],{"class":701},[510,1798,882],{"class":565},[510,1800,385],{"class":701},[510,1802,887],{"class":565},[510,1804,385],{"class":701},[510,1806,892],{"class":565},[510,1808,385],{"class":701},[510,1810,666],{"class":565},[510,1812,899],{"class":701},[510,1814,1815,1817,1819,1821,1823,1825,1827,1829,1831,1833,1835],{"class":512,"line":532},[510,1816,517],{"class":516},[510,1818,877],{"class":565},[510,1820,385],{"class":701},[510,1822,882],{"class":565},[510,1824,385],{"class":701},[510,1826,887],{"class":565},[510,1828,385],{"class":701},[510,1830,892],{"class":565},[510,1832,385],{"class":701},[510,1834,738],{"class":565},[510,1836,899],{"class":701},[510,1838,1839,1841,1843,1845,1847,1849,1851,1853,1855,1857,1859],{"class":512,"line":540},[510,1840,517],{"class":516},[510,1842,877],{"class":565},[510,1844,385],{"class":701},[510,1846,882],{"class":565},[510,1848,385],{"class":701},[510,1850,887],{"class":565},[510,1852,385],{"class":701},[510,1854,966],{"class":565},[510,1856,385],{"class":701},[510,1858,365],{"class":565},[510,1860,899],{"class":701},[510,1862,1863,1865,1867,1869,1871,1873,1875,1877,1879,1881,1883],{"class":512,"line":548},[510,1864,517],{"class":516},[510,1866,877],{"class":565},[510,1868,385],{"class":701},[510,1870,882],{"class":565},[510,1872,385],{"class":701},[510,1874,887],{"class":565},[510,1876,385],{"class":701},[510,1878,966],{"class":565},[510,1880,385],{"class":701},[510,1882,632],{"class":565},[510,1884,899],{"class":701},[510,1886,1887],{"class":512,"line":555},[510,1888,552],{"emptyLinePlaceholder":551},[510,1890,1891,1893,1895],{"class":512,"line":569},[510,1892,981],{"class":572},[510,1894,984],{"class":572},[510,1896,1897],{"class":561}," TransferProcessor\n",[510,1899,1900,1902,1904,1906,1908,1910,1913,1915,1917,1919,1921,1923],{"class":512,"line":588},[510,1901,992],{"class":572},[510,1903,995],{"class":520},[510,1905,597],{"class":701},[510,1907,582],{"class":572},[510,1909,1002],{"class":701},[510,1911,1912],{"class":572}," Transfer",[510,1914,1002],{"class":701},[510,1916,1010],{"class":572},[510,1918,1002],{"class":701},[510,1920,1912],{"class":572},[510,1922,1017],{"class":701},[510,1924,1020],{"class":701},[510,1926,1927],{"class":512,"line":618},[510,1928,552],{"emptyLinePlaceholder":551},[510,1930,1931,1933,1935,1937,1939],{"class":512,"line":646},[510,1932,573],{"class":572},[510,1934,1031],{"class":572},[510,1936,1010],{"class":565},[510,1938,1037],{"class":1036},[510,1940,899],{"class":701},[510,1942,1943,1945,1947,1949,1951,1953,1955,1957,1960],{"class":512,"line":651},[510,1944,573],{"class":572},[510,1946,1046],{"class":565},[510,1948,597],{"class":701},[510,1950,582],{"class":572},[510,1952,1002],{"class":701},[510,1954,1005],{"class":572},[510,1956,1017],{"class":701},[510,1958,1959],{"class":1036}," balances",[510,1961,899],{"class":701},[510,1963,1964],{"class":512,"line":680},[510,1965,552],{"emptyLinePlaceholder":551},[510,1967,1968],{"class":512,"line":695},[510,1969,1542],{"class":754},[510,1971,1972,1974,1976,1978,1981,1983,1986,1988],{"class":512,"line":714},[510,1973,573],{"class":572},[510,1975,1031],{"class":572},[510,1977,1556],{"class":565},[510,1979,1980],{"class":1036}," keyLocks",[510,1982,1098],{"class":701},[510,1984,1985],{"class":516}," new",[510,1987,1556],{"class":520},[510,1989,1323],{"class":701},[510,1991,1992],{"class":512,"line":720},[510,1993,552],{"emptyLinePlaceholder":551},[510,1995,1996,1998,2000,2002,2004,2006,2008],{"class":512,"line":725},[510,1997,1070],{"class":572},[510,1999,1471],{"class":520},[510,2001,1075],{"class":701},[510,2003,1078],{"class":565},[510,2005,1081],{"class":1036},[510,2007,1084],{"class":701},[510,2009,1020],{"class":701},[510,2011,2012,2014,2016,2018,2020,2022],{"class":512,"line":751},[510,2013,1091],{"class":683},[510,2015,385],{"class":701},[510,2017,1081],{"class":1036},[510,2019,1098],{"class":701},[510,2021,1037],{"class":565},[510,2023,899],{"class":701},[510,2025,2026],{"class":512,"line":758},[510,2027,717],{"class":701},[510,2029,2030],{"class":512,"line":764},[510,2031,552],{"emptyLinePlaceholder":551},[510,2033,2034,2036],{"class":512,"line":794},[510,2035,1115],{"class":701},[510,2037,1118],{"class":572},[510,2039,2040,2042,2044,2046,2048,2050,2052,2054,2056,2058,2060,2062,2064],{"class":512,"line":818},[510,2041,1070],{"class":572},[510,2043,1125],{"class":572},[510,2045,660],{"class":520},[510,2047,1075],{"class":701},[510,2049,666],{"class":565},[510,2051,597],{"class":701},[510,2053,582],{"class":572},[510,2055,1002],{"class":701},[510,2057,1912],{"class":572},[510,2059,1017],{"class":701},[510,2061,1144],{"class":1036},[510,2063,1084],{"class":701},[510,2065,1020],{"class":701},[510,2067,2068,2070,2072,2074,2076,2078],{"class":512,"line":834},[510,2069,684],{"class":683},[510,2071,385],{"class":701},[510,2073,689],{"class":520},[510,2075,1075],{"class":701},[510,2077,1161],{"class":565},[510,2079,1164],{"class":701},[510,2081,2082,2084,2086,2089,2091,2093,2095,2097,2099,2101],{"class":512,"line":854},[510,2083,1091],{"class":683},[510,2085,385],{"class":701},[510,2087,2088],{"class":1036},"balances",[510,2090,1098],{"class":701},[510,2092,1144],{"class":1036},[510,2094,385],{"class":701},[510,2096,708],{"class":520},[510,2098,1075],{"class":701},[510,2100,1081],{"class":565},[510,2102,1164],{"class":701},[510,2104,2105],{"class":512,"line":859},[510,2106,717],{"class":701},[510,2108,2109],{"class":512,"line":1237},[510,2110,552],{"emptyLinePlaceholder":551},[510,2112,2113,2115],{"class":512,"line":1242},[510,2114,1115],{"class":701},[510,2116,1118],{"class":572},[510,2118,2119,2121,2123,2125,2127,2129,2131,2133,2135,2137,2139,2141,2143],{"class":512,"line":1303},[510,2120,1070],{"class":572},[510,2122,1125],{"class":572},[510,2124,732],{"class":520},[510,2126,1075],{"class":701},[510,2128,738],{"class":565},[510,2130,597],{"class":701},[510,2132,582],{"class":572},[510,2134,1002],{"class":701},[510,2136,1912],{"class":572},[510,2138,1017],{"class":701},[510,2140,1226],{"class":1036},[510,2142,1084],{"class":701},[510,2144,1020],{"class":701},[510,2146,2147,2150,2153,2155,2157,2159,2161],{"class":512,"line":1326},[510,2148,2149],{"class":565},"        Transfer",[510,2151,2152],{"class":1036}," t",[510,2154,1098],{"class":701},[510,2156,1226],{"class":1036},[510,2158,385],{"class":701},[510,2160,812],{"class":520},[510,2162,1323],{"class":701},[510,2164,2165],{"class":512,"line":1351},[510,2166,1657],{"class":754},[510,2168,2169,2172,2174,2176,2178,2181,2184,2187,2189,2191,2194,2196,2198,2200,2203,2206,2209,2212],{"class":512,"line":1376},[510,2170,2171],{"class":1036},"        keyLocks",[510,2173,385],{"class":701},[510,2175,1665],{"class":520},[510,2177,1075],{"class":701},[510,2179,2180],{"class":516},"new",[510,2182,2183],{"class":572}," Object",[510,2185,2186],{"class":701},"[]{",[510,2188,2152],{"class":1036},[510,2190,385],{"class":701},[510,2192,2193],{"class":1036},"from",[510,2195,1002],{"class":701},[510,2197,2152],{"class":1036},[510,2199,385],{"class":701},[510,2201,2202],{"class":1036},"to",[510,2204,2205],{"class":701}," },",[510,2207,2208],{"class":701}," ()",[510,2210,2211],{"class":701}," ->",[510,2213,1020],{"class":701},[510,2215,2216,2219,2222,2224,2226,2228,2230,2232,2235,2237,2239,2241,2243,2245,2247,2249,2251,2253,2255,2257,2259,2261,2263,2265],{"class":512,"line":1381},[510,2217,2218],{"class":572},"            long",[510,2220,2221],{"class":1036}," fromBalance",[510,2223,1098],{"class":701},[510,2225,1959],{"class":1036},[510,2227,385],{"class":701},[510,2229,778],{"class":520},[510,2231,1075],{"class":701},[510,2233,2234],{"class":1036},"t",[510,2236,385],{"class":701},[510,2238,2193],{"class":1036},[510,2240,1084],{"class":701},[510,2242,1271],{"class":572},[510,2244,1274],{"class":516},[510,2246,1277],{"class":516},[510,2248,1280],{"class":790},[510,2250,1283],{"class":516},[510,2252,1959],{"class":1036},[510,2254,385],{"class":701},[510,2256,778],{"class":520},[510,2258,1075],{"class":701},[510,2260,2234],{"class":1036},[510,2262,385],{"class":701},[510,2264,2193],{"class":1036},[510,2266,1164],{"class":701},[510,2268,2270,2272,2275,2277,2279,2281,2283,2285,2287,2289,2291,2293,2295,2297,2299,2301,2303,2305,2307,2309,2311,2313,2315,2317],{"class":512,"line":2269},32,[510,2271,2218],{"class":572},[510,2273,2274],{"class":1036}," toBalance",[510,2276,1098],{"class":701},[510,2278,1959],{"class":1036},[510,2280,385],{"class":701},[510,2282,778],{"class":520},[510,2284,1075],{"class":701},[510,2286,2234],{"class":1036},[510,2288,385],{"class":701},[510,2290,2202],{"class":1036},[510,2292,1084],{"class":701},[510,2294,1271],{"class":572},[510,2296,1274],{"class":516},[510,2298,1277],{"class":516},[510,2300,1280],{"class":790},[510,2302,1283],{"class":516},[510,2304,1959],{"class":1036},[510,2306,385],{"class":701},[510,2308,778],{"class":520},[510,2310,1075],{"class":701},[510,2312,2234],{"class":1036},[510,2314,385],{"class":701},[510,2316,2202],{"class":1036},[510,2318,1164],{"class":701},[510,2320,2322,2325,2327,2329,2331,2333,2335,2337,2339,2341,2343,2345,2347,2350],{"class":512,"line":2321},33,[510,2323,2324],{"class":1036},"            balances",[510,2326,385],{"class":701},[510,2328,824],{"class":520},[510,2330,1075],{"class":701},[510,2332,2234],{"class":1036},[510,2334,385],{"class":701},[510,2336,2193],{"class":1036},[510,2338,1002],{"class":701},[510,2340,1676],{"class":565},[510,2342,1719],{"class":572},[510,2344,2152],{"class":1036},[510,2346,385],{"class":701},[510,2348,2349],{"class":1036},"amount",[510,2351,1164],{"class":701},[510,2353,2355,2357,2359,2361,2363,2365,2367,2369,2371,2373,2375,2377,2379,2381],{"class":512,"line":2354},34,[510,2356,2324],{"class":1036},[510,2358,385],{"class":701},[510,2360,824],{"class":520},[510,2362,1075],{"class":701},[510,2364,2234],{"class":1036},[510,2366,385],{"class":701},[510,2368,2202],{"class":1036},[510,2370,1002],{"class":701},[510,2372,1695],{"class":565},[510,2374,806],{"class":572},[510,2376,2152],{"class":1036},[510,2378,385],{"class":701},[510,2380,2349],{"class":1036},[510,2382,1164],{"class":701},[510,2384,2386,2389,2391],{"class":512,"line":2385},35,[510,2387,2388],{"class":516},"            return",[510,2390,1274],{"class":516},[510,2392,899],{"class":701},[510,2394,2396],{"class":512,"line":2395},36,[510,2397,2398],{"class":701},"        });\n",[510,2400,2402,2404,2406,2408,2410,2412],{"class":512,"line":2401},37,[510,2403,837],{"class":520},[510,2405,840],{"class":701},[510,2407,843],{"class":520},[510,2409,1075],{"class":701},[510,2411,1261],{"class":565},[510,2413,1164],{"class":701},[510,2415,2417],{"class":512,"line":2416},38,[510,2418,717],{"class":701},[510,2420,2422],{"class":512,"line":2421},39,[510,2423,862],{"class":701},[2425,2426,2428],"callout",{"color":2427,"icon":80},"warning",[320,2429,2430,2431,2434,2435,2437,2438,2441,2442,2445,2446,2448],{},"The block you pass to ",[354,2432,2433],{},"withLock"," \u002F ",[354,2436,1665],{}," ",[324,2439,2440],{},"must be fast and non-blocking",". Holding a lock across an I\u002FO call, an external lookup, or any blocking operation stalls other lanes and can hold up the commit barrier. Do the blocking work ",[359,2443,2444],{},"before"," you enter the lock; keep the locked section to the in-memory read-modify-write. ",[354,2447,1665],{}," accepts at most 8 keys — if you need more, the access pattern likely wants redesigning.",[320,2450,2451,2452,1418,2455,2458],{},"If a lock can't be acquired within the configured timeout (default 10 seconds), the call throws ",[354,2453,2454],{},"KeyLockTimeoutException",[354,2456,2457],{},"io.stoatflow.core.exception",") rather than blocking forever — a loud signal of a deadlock or excessive contention rather than a silent stall.",[2425,2460,2463],{"color":2461,"icon":2462},"info","i-lucide-info",[320,2464,2465,2466,2468],{},"You almost never need this. Single-key updates — which is what the DSL aggregations, joins, and the overwhelming majority of custom processors do — are already safe through key affinity alone. Reach for ",[354,2467,365],{}," only when a genuine cross-key invariant exists.",[387,2470,2472],{"id":2471},"store-types","Store types",[320,2474,2475,2476,481,2478,481,2481,2483,2484,2486],{},"StoatFlow ships five store types. Stateful DSL operators (",[354,2477,480],{},[354,2479,2480],{},"reduce",[354,2482,484],{},", joins, windowed and session aggregations, suppress) pick the appropriate type automatically; custom ",[354,2485,356],{},"s declare the stores they need. Each type comes in two backends:",[332,2488,2489,2495],{},[335,2490,2491,2494],{},[324,2492,2493],{},"RocksDB-backed"," — persistent, on-disk. The default. State larger than memory is fine; local data survives process restarts and gives the changelog a head-start on recovery.",[335,2496,2497,2500],{},[324,2498,2499],{},"In-memory"," — held entirely in the JVM heap. Lower per-operation overhead; bounded by available memory. Still durable via the changelog — the in-memory store is rebuilt from it on restart.",[2502,2503,2504,2520],"table",{},[2505,2506,2507],"thead",{},[2508,2509,2510,2514,2517],"tr",{},[2511,2512,2513],"th",{},"Store type",[2511,2515,2516],{},"What it holds",[2511,2518,2519],{},"Typical use",[2521,2522,2523,2546,2559,2572,2585],"tbody",{},[2508,2524,2525,2531,2534],{},[2526,2527,2528],"td",{},[324,2529,2530],{},"Key-value",[2526,2532,2533],{},"One value per key",[2526,2535,2536,481,2538,481,2540,2542,2543,2545],{},[354,2537,480],{},[354,2539,2480],{},[354,2541,484],{},", general ",[354,2544,356],{}," state",[2508,2547,2548,2553,2556],{},[2526,2549,2550],{},[324,2551,2552],{},"Window",[2526,2554,2555],{},"Values bucketed by time window",[2526,2557,2558],{},"Windowed aggregations, tumbling\u002Fhopping\u002Fsliding windows",[2508,2560,2561,2566,2569],{},[2526,2562,2563],{},[324,2564,2565],{},"Session",[2526,2567,2568],{},"Values grouped into activity-gap sessions",[2526,2570,2571],{},"Session-window aggregations",[2508,2573,2574,2579,2582],{},[2526,2575,2576],{},[324,2577,2578],{},"Versioned",[2526,2580,2581],{},"Historical values queryable by timestamp (KIP-889)",[2526,2583,2584],{},"Point-in-time lookups, temporal joins",[2508,2586,2587,2592,2595],{},[2526,2588,2589],{},[324,2590,2591],{},"Timer",[2526,2593,2594],{},"Scheduled callbacks keyed by fire time",[2526,2596,2597,2599],{},[354,2598,356],{}," event-time \u002F processing-time timers",[320,2601,2602,2603,2606,2607,2610,2611,385],{},"The backend is chosen per store. The DSL uses RocksDB by default; you switch a materialized store to in-memory with ",[354,2604,2605],{},"Materialized.withStoreType(StoreType.IN_MEMORY)",", and custom processors choose via the ",[354,2608,2609],{},"Stores"," factory. The full how-to — declaring stores, the precedence rules, and the factory API — is on ",[377,2612,100],{"href":101},[387,2614,2616],{"id":2615},"where-to-go-next","Where to go next",[332,2618,2619,2626,2631,2636],{},[335,2620,2621,2623,2624],{},[377,2622,100],{"href":101}," — declaring stores, choosing types, and accessing them from the DSL or a ",[354,2625,356],{},[335,2627,2628,2630],{},[377,2629,57],{"href":58}," — the key-affinity routing that makes concurrent state access safe",[335,2632,2633,2635],{},[377,2634,47],{"href":48}," — the single-instance engine, commit barrier, and changelog durability",[335,2637,2638,2641],{},[377,2639,2640],{"href":136},"Processor API"," — building custom processors that own their state",[2643,2644,2645],"style",{},"html pre.shiki code .sbBg2, html code.shiki .sbBg2{--shiki-default:#1E754F}html pre.shiki code .sySUi, html code.shiki .sySUi{--shiki-default:#59873A}html pre.shiki code .sUxyF, html code.shiki .sUxyF{--shiki-default:#2E8F82}html pre.shiki code .suHK_, html code.shiki .suHK_{--shiki-default:#393A34}html pre.shiki code .si04Y, html code.shiki .si04Y{--shiki-default:#AB5959}html pre.shiki code .sEi1f, html code.shiki .sEi1f{--shiki-default:#A65E2B}html pre.shiki code .sYZai, html code.shiki .sYZai{--shiki-default:#999999}html pre.shiki code .s8zF2, html code.shiki .s8zF2{--shiki-default:#A0ADA0}html pre.shiki code .s-TwI, html code.shiki .s-TwI{--shiki-default:#2F798A}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 pre.shiki code .svycV, html code.shiki .svycV{--shiki-default:#B07D48}",{"title":506,"searchDepth":524,"depth":524,"links":2647},[2648,2649,2650,2651,2652],{"id":389,"depth":524,"text":390},{"id":430,"depth":524,"text":431},{"id":1386,"depth":524,"text":1387},{"id":2471,"depth":524,"text":2472},{"id":2615,"depth":524,"text":2616},"How StoatFlow's global state model works — why concurrent access across lanes is safe, when you need the key-lock utility, and which store types are available.","md",{},{"icon":65},"explanation",{"title":62,"description":2653},"LgxxhPjgKdvuGQNGEsJzLHSmWT4VuW2eo8XWDP1robo",[2661,2663],{"title":57,"path":58,"stem":59,"description":2662,"icon":60,"children":-1},"How key-affinity lanes give StoatFlow per-key ordering and cross-key parallelism — decoupled from partition count, cheap to scale with cores, and friendly to in-line blocking I\u002FO.",{"title":67,"path":68,"stem":69,"description":2664,"icon":70,"children":-1},"How StoatFlow models time — event time vs processing time, record timestamps, watermark strategies, window closing, late-record handling, and event\u002Fprocessing-time timers.",1783713351857]