Description
StateManagerDisk.set_state only adds a token to _write_queue if that token is not already queued. When disk write debouncing is enabled, a second set_state call for the same token before the queued item is flushed does not replace the queued state.
That means the disk backend can persist an older state snapshot even though a newer set_state call completed successfully.
Relevant code path:
if self._write_debounce_seconds > 0:
if token not in self._write_queue:
self._write_queue[token] = QueueItem(
token=token,
state=state,
timestamp=time.time(),
)
Because the existing QueueItem is left unchanged, _flush_write_queue / _process_write_queue later writes the first queued value, not the latest value.
Reproduction
import asyncio
import tempfile
from pathlib import Path
from reflex.istate.manager.disk import StateManagerDisk
from reflex.istate.manager.token import StateToken
async def main():
with tempfile.TemporaryDirectory() as tmp:
manager = StateManagerDisk()
manager.__dict__["states_directory"] = Path(tmp)
manager._write_debounce_seconds = 60
token = StateToken(ident="client", cls=int)
await manager.set_state(token, 1)
await manager.set_state(token, 2)
print("queued_state", manager._write_queue[token].state)
await manager.close()
reloaded = StateManagerDisk()
reloaded.__dict__["states_directory"] = Path(tmp)
print("persisted", await reloaded.load_state(token))
await reloaded.close()
asyncio.run(main())
Current output:
queued_state 1
persisted 1
Expected behavior
The latest completed set_state call should win. The queued and persisted value should be 2.
Actual behavior
The first queued value is retained and persisted. Later set_state calls for the same token during the debounce window are ignored by the write queue.
Impact
This is backend state persistence behavior specific to the disk state manager. It can cause stale state to be written to disk when callers use direct set_state with replacement state objects while debounced writes are pending.
The common modify_state path may hide this when the queued object is mutated in place, but direct replacement through set_state can lose the newer value.
Possible fix
When a token is already queued, update the queued item to reference the latest state. Depending on intended debounce semantics, either preserve the original timestamp to flush at the first scheduled time or refresh the timestamp to debounce from the latest update. The key requirement is that the queued state object must be the latest completed set_state value.
Duplicate check
I searched for existing open/closed issues and PRs using terms around StateManagerDisk, _write_queue, debounce, set_state, stale state, and lost disk persistence, and did not find an existing report for this behavior.
Description
StateManagerDisk.set_stateonly adds a token to_write_queueif that token is not already queued. When disk write debouncing is enabled, a secondset_statecall for the same token before the queued item is flushed does not replace the queued state.That means the disk backend can persist an older state snapshot even though a newer
set_statecall completed successfully.Relevant code path:
Because the existing
QueueItemis left unchanged,_flush_write_queue/_process_write_queuelater writes the first queued value, not the latest value.Reproduction
Current output:
Expected behavior
The latest completed
set_statecall should win. The queued and persisted value should be2.Actual behavior
The first queued value is retained and persisted. Later
set_statecalls for the same token during the debounce window are ignored by the write queue.Impact
This is backend state persistence behavior specific to the disk state manager. It can cause stale state to be written to disk when callers use direct
set_statewith replacement state objects while debounced writes are pending.The common
modify_statepath may hide this when the queued object is mutated in place, but direct replacement throughset_statecan lose the newer value.Possible fix
When a token is already queued, update the queued item to reference the latest state. Depending on intended debounce semantics, either preserve the original timestamp to flush at the first scheduled time or refresh the timestamp to debounce from the latest update. The key requirement is that the queued state object must be the latest completed
set_statevalue.Duplicate check
I searched for existing open/closed issues and PRs using terms around
StateManagerDisk,_write_queue,debounce,set_state, stale state, and lost disk persistence, and did not find an existing report for this behavior.