Skip to content

Commit c278ea4

Browse files
committed
fix: Raise SyncNotRunningError for checkpoints outside their sync, and skip unchanged state values
1 parent 221a722 commit c278ea4

4 files changed

Lines changed: 48 additions & 4 deletions

File tree

‎cloudquery/sdk/internal/checkpoint.py‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
from cloudquery.sdk.message import SyncCheckpointMessage
2323

2424

25+
class SyncNotRunningError(RuntimeError):
26+
"""
27+
Raised when state checkpoints are flushed while their sync is not running, so they cannot be
28+
sent: before or after Scheduler.sync, or from a resolver of an ended sync. Flush state that
29+
summarizes the whole sync in the scheduler's post-sync hook instead.
30+
"""
31+
32+
2533
class Scope:
2634
"""
2735
A table resolver call and the relations its resources spawn.
@@ -170,17 +178,17 @@ def schedule(
170178
wait for nothing. Schedules all of them or, if it raises, none of them.
171179
172180
Raises:
173-
RuntimeError: if no sync is running, as the checkpoints could not be sent.
181+
SyncNotRunningError: if the sync of the checkpoints is not running.
174182
"""
175183
with self._lock:
176184
if self._res is None:
177-
raise RuntimeError(
185+
raise SyncNotRunningError(
178186
"state checkpoints can only be flushed while the scheduler syncs, "
179187
"e.g. from a table resolver or the post-sync hook"
180188
)
181189
for scopes, _, _ in checkpoints:
182190
if any(s is not None and s.sync is not self._sync for s in scopes):
183-
raise RuntimeError(
191+
raise SyncNotRunningError(
184192
"state checkpoints of a table resolver can only be flushed during its own sync"
185193
)
186194
for scopes, keys, msg in checkpoints:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
from cloudquery.sdk.internal.checkpoint import SyncNotRunningError
12
from .keys import state_key

‎cloudquery/sdk/stateclient/stateclient.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ def get_key(self, key: str) -> Optional[str]:
8787
def set_key(self, key: str, value: str) -> None:
8888
scope = self.checkpoints.current_scope() if self.checkpoints else None
8989
with self._lock:
90+
if self.mem.get(key) == value:
91+
# a pending change of the key summarizes this caller's resources too
92+
if key in self.changes:
93+
self.changes[key].add(scope)
94+
return
9095
self.mem[key] = value
9196
self.changes.setdefault(key, set()).add(scope)
9297

@@ -95,6 +100,10 @@ def flush(self):
95100
Persists the keys changed since the last flush: writes them to the backend, or, with
96101
checkpoints, sends them on the sync stream after the resources they cover.
97102
If that fails, the keys stay changed, so a later flush retries them.
103+
104+
Raises:
105+
SyncNotRunningError: with checkpoints, if the sync is not running, e.g. once
106+
Scheduler.sync returned. Flush in the scheduler's post-sync hook instead.
98107
"""
99108
with self._flush_lock:
100109
with self._lock:

‎tests/scheduler/checkpoints.py‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from cloudquery.sdk.schema import Column
1919
from cloudquery.sdk.schema.arrow import METADATA_TABLE_NAME
2020
from cloudquery.sdk.schema.table import Table
21+
from cloudquery.sdk.stateclient import SyncNotRunningError
2122
from cloudquery.sdk.stateclient.stateclient import (
2223
ConnectedStateClient,
2324
StateClient,
@@ -223,6 +224,31 @@ def test_flush_writes_to_the_backend_without_checkpoints():
223224
assert state.writes[-1] == [("a", "3")]
224225

225226

227+
def test_setting_an_unchanged_value_is_not_a_change():
228+
state = MemoryStateClient(checkpoints=None)
229+
state.backend["a"] = "1"
230+
state.read_all_state()
231+
state.set_key("a", "1")
232+
state.flush()
233+
assert state.writes == []
234+
235+
state.set_key("a", "2")
236+
state.set_key("a", "2")
237+
state.flush()
238+
assert state.writes == [[("a", "2")]]
239+
240+
241+
def test_setting_an_unchanged_value_adds_the_caller_to_the_pending_change():
242+
scheduler = Scheduler(1)
243+
state = MemoryStateClient(checkpoints=scheduler.checkpoints)
244+
state.set_key("a", "1")
245+
scope = scheduler.checkpoints.new_scope(None)
246+
with scheduler.checkpoints.entered(scope):
247+
state.set_key("a", "1")
248+
scheduler.shutdown()
249+
assert state.changes["a"] == {None, scope}
250+
251+
226252
def test_failed_flush_keeps_the_changes():
227253
state = MemoryStateClient(checkpoints=None, fail_writes=True)
228254
state.set_key("a", "1")
@@ -238,7 +264,7 @@ def test_checkpoint_flush_outside_a_sync_fails_and_keeps_the_changes():
238264
scheduler = Scheduler(1)
239265
state = MemoryStateClient(checkpoints=scheduler.checkpoints)
240266
state.set_key("a", "1")
241-
with pytest.raises(RuntimeError):
267+
with pytest.raises(SyncNotRunningError):
242268
state.flush()
243269
assert "a" in state.changes
244270
assert state.writes == []

0 commit comments

Comments
 (0)