dory-processor-sdk 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dory/__init__.py +101 -0
- dory/auth/__init__.py +10 -0
- dory/auth/oauth2.py +153 -0
- dory/auto_instrument.py +142 -0
- dory/cli/__init__.py +5 -0
- dory/cli/main.py +137 -0
- dory/cli/templates.py +123 -0
- dory/config/__init__.py +23 -0
- dory/config/defaults.py +24 -0
- dory/config/loader.py +430 -0
- dory/config/presets.py +73 -0
- dory/config/schema.py +84 -0
- dory/core/__init__.py +27 -0
- dory/core/app.py +434 -0
- dory/core/context.py +209 -0
- dory/core/lifecycle.py +214 -0
- dory/core/meta.py +121 -0
- dory/core/modes.py +479 -0
- dory/core/processor.py +564 -0
- dory/core/signals.py +122 -0
- dory/decorators.py +142 -0
- dory/edge/__init__.py +88 -0
- dory/edge/adaptive.py +644 -0
- dory/edge/detector.py +546 -0
- dory/edge/fencing.py +488 -0
- dory/edge/heartbeat.py +598 -0
- dory/edge/role.py +419 -0
- dory/errors/__init__.py +139 -0
- dory/errors/classification.py +362 -0
- dory/errors/codes.py +498 -0
- dory/geo/__init__.py +40 -0
- dory/geo/geolocalizer.py +1034 -0
- dory/health/__init__.py +12 -0
- dory/health/probes.py +210 -0
- dory/health/server.py +635 -0
- dory/k8s/__init__.py +80 -0
- dory/k8s/annotation_watcher.py +184 -0
- dory/k8s/client.py +251 -0
- dory/k8s/labels.py +505 -0
- dory/k8s/pod_metadata.py +182 -0
- dory/logging/__init__.py +9 -0
- dory/logging/logger.py +148 -0
- dory/metrics/__init__.py +7 -0
- dory/metrics/collector.py +301 -0
- dory/middleware/__init__.py +46 -0
- dory/middleware/connection_tracker.py +608 -0
- dory/middleware/request_id.py +325 -0
- dory/middleware/request_tracker.py +511 -0
- dory/migration/__init__.py +33 -0
- dory/migration/configmap.py +232 -0
- dory/migration/s3_store.py +594 -0
- dory/migration/serialization.py +135 -0
- dory/migration/state_manager.py +286 -0
- dory/migration/transfer.py +382 -0
- dory/monitoring/__init__.py +29 -0
- dory/monitoring/opentelemetry.py +489 -0
- dory/output/__init__.py +31 -0
- dory/output/envelope.py +137 -0
- dory/output/formatter.py +113 -0
- dory/output/rabbitmq.py +632 -0
- dory/output/routing.py +318 -0
- dory/output/validator.py +199 -0
- dory/py.typed +2 -0
- dory/recovery/__init__.py +60 -0
- dory/recovery/golden_image.py +487 -0
- dory/recovery/golden_snapshot.py +713 -0
- dory/recovery/golden_validator.py +518 -0
- dory/recovery/partial_recovery.py +482 -0
- dory/recovery/recovery_decision.py +242 -0
- dory/recovery/restart_detector.py +142 -0
- dory/recovery/state_validator.py +183 -0
- dory/resilience/__init__.py +45 -0
- dory/resilience/circuit_breaker.py +457 -0
- dory/resilience/retry.py +389 -0
- dory/simple.py +342 -0
- dory/types.py +68 -0
- dory/utils/__init__.py +31 -0
- dory/utils/errors.py +59 -0
- dory/utils/retry.py +115 -0
- dory/utils/timeout.py +80 -0
- dory_processor_sdk-0.0.1.dist-info/METADATA +424 -0
- dory_processor_sdk-0.0.1.dist-info/RECORD +86 -0
- dory_processor_sdk-0.0.1.dist-info/WHEEL +5 -0
- dory_processor_sdk-0.0.1.dist-info/entry_points.txt +2 -0
- dory_processor_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
- dory_processor_sdk-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
State serialization utilities.
|
|
3
|
+
|
|
4
|
+
Handles JSON serialization/deserialization with checksum validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from dory.utils.errors import DoryStateError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class StateEnvelope:
|
|
18
|
+
"""
|
|
19
|
+
Envelope wrapping state data with metadata.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
payload: The actual state data
|
|
23
|
+
metadata: Metadata about when/where state was created
|
|
24
|
+
checksum: SHA256 checksum of payload for integrity
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
payload: dict[str, Any]
|
|
28
|
+
metadata: dict[str, Any]
|
|
29
|
+
checksum: str
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict[str, Any]:
|
|
32
|
+
"""Convert to dictionary for serialization."""
|
|
33
|
+
return {
|
|
34
|
+
"payload": self.payload,
|
|
35
|
+
"metadata": self.metadata,
|
|
36
|
+
"checksum": self.checksum,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_dict(cls, data: dict[str, Any]) -> "StateEnvelope":
|
|
41
|
+
"""Create from dictionary."""
|
|
42
|
+
return cls(
|
|
43
|
+
payload=data["payload"],
|
|
44
|
+
metadata=data["metadata"],
|
|
45
|
+
checksum=data["checksum"],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StateSerializer:
|
|
50
|
+
"""
|
|
51
|
+
Serializes and deserializes state with integrity checking.
|
|
52
|
+
|
|
53
|
+
Uses JSON format with SHA256 checksums for integrity validation.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def compute_checksum(payload: dict[str, Any]) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Compute SHA256 checksum for payload.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
payload: State payload
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Hex-encoded SHA256 checksum
|
|
66
|
+
"""
|
|
67
|
+
payload_json = json.dumps(payload, sort_keys=True)
|
|
68
|
+
return hashlib.sha256(payload_json.encode()).hexdigest()
|
|
69
|
+
|
|
70
|
+
def serialize(
|
|
71
|
+
self,
|
|
72
|
+
state: dict[str, Any],
|
|
73
|
+
processor_id: str,
|
|
74
|
+
pod_name: str,
|
|
75
|
+
restart_count: int = 0,
|
|
76
|
+
) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Serialize state to JSON string with envelope.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
state: State dictionary to serialize
|
|
82
|
+
processor_id: Processor ID for metadata
|
|
83
|
+
pod_name: Pod name for metadata
|
|
84
|
+
restart_count: Current restart count
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
JSON string with state envelope
|
|
88
|
+
"""
|
|
89
|
+
envelope = StateEnvelope(
|
|
90
|
+
payload=state,
|
|
91
|
+
metadata={
|
|
92
|
+
"timestamp": time.time(),
|
|
93
|
+
"timestamp_iso": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
94
|
+
"processor_id": processor_id,
|
|
95
|
+
"pod_name": pod_name,
|
|
96
|
+
"restart_count": restart_count,
|
|
97
|
+
},
|
|
98
|
+
checksum=self.compute_checksum(state),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return json.dumps(envelope.to_dict(), indent=2)
|
|
102
|
+
|
|
103
|
+
def deserialize(self, data: str) -> dict[str, Any]:
|
|
104
|
+
"""
|
|
105
|
+
Deserialize state from JSON string.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
data: JSON string with state envelope
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
State payload dictionary
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
DoryStateError: If deserialization or validation fails
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
envelope_dict = json.loads(data)
|
|
118
|
+
except json.JSONDecodeError as e:
|
|
119
|
+
raise DoryStateError(f"Invalid JSON in state data: {e}", cause=e)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
envelope = StateEnvelope.from_dict(envelope_dict)
|
|
123
|
+
except KeyError as e:
|
|
124
|
+
raise DoryStateError(f"Missing field in state envelope: {e}", cause=e)
|
|
125
|
+
|
|
126
|
+
# Validate checksum
|
|
127
|
+
expected_checksum = self.compute_checksum(envelope.payload)
|
|
128
|
+
if envelope.checksum != expected_checksum:
|
|
129
|
+
raise DoryStateError(
|
|
130
|
+
f"State checksum mismatch: expected {expected_checksum}, "
|
|
131
|
+
f"got {envelope.checksum}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return envelope.payload
|
|
135
|
+
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StateManager - High-level state management for migrations.
|
|
3
|
+
|
|
4
|
+
Provides unified interface for state operations across different
|
|
5
|
+
storage backends (ConfigMap, S3, local file).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from dory.types import StateBackend
|
|
14
|
+
from dory.migration.serialization import StateSerializer
|
|
15
|
+
from dory.migration.configmap import ConfigMapStore
|
|
16
|
+
from dory.migration.s3_store import S3Store, S3Config
|
|
17
|
+
from dory.utils.errors import DoryStateError
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from dory.config.schema import DoryConfig
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StateManager:
|
|
26
|
+
"""
|
|
27
|
+
High-level state management for processor migrations.
|
|
28
|
+
|
|
29
|
+
Supports multiple backends:
|
|
30
|
+
- ConfigMap: Kubernetes ConfigMap (default, <1MB)
|
|
31
|
+
- S3: AWS S3 (for multi-cluster)
|
|
32
|
+
- Local/PVC: Local file (tries /data first, falls back to /tmp)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
LOCAL_STATE_PATH = "/data/dory-state.json"
|
|
36
|
+
LOCAL_STATE_PATH_FALLBACK = "/tmp/dory-state.json"
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
backend: str | StateBackend = StateBackend.CONFIGMAP,
|
|
41
|
+
config: "DoryConfig | None" = None,
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Initialize state manager.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
backend: Storage backend to use
|
|
48
|
+
config: SDK configuration
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(backend, str):
|
|
51
|
+
backend = StateBackend(backend)
|
|
52
|
+
|
|
53
|
+
self._backend = backend
|
|
54
|
+
self._config = config
|
|
55
|
+
self._serializer = StateSerializer()
|
|
56
|
+
self._configmap_store: ConfigMapStore | None = None
|
|
57
|
+
self._s3_store: S3Store | None = None
|
|
58
|
+
|
|
59
|
+
# Get namespace from environment
|
|
60
|
+
self._namespace = os.environ.get("POD_NAMESPACE", "default")
|
|
61
|
+
self._pod_name = os.environ.get("POD_NAME", "unknown")
|
|
62
|
+
|
|
63
|
+
async def save_state(
|
|
64
|
+
self,
|
|
65
|
+
processor_id: str,
|
|
66
|
+
state: dict[str, Any],
|
|
67
|
+
restart_count: int = 0,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Save processor state.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
processor_id: Processor ID
|
|
74
|
+
state: State dictionary to save
|
|
75
|
+
restart_count: Current restart count for metadata
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
DoryStateError: If save fails
|
|
79
|
+
"""
|
|
80
|
+
logger.debug(f"Saving state for processor {processor_id}")
|
|
81
|
+
|
|
82
|
+
# Serialize state
|
|
83
|
+
state_json = self._serializer.serialize(
|
|
84
|
+
state=state,
|
|
85
|
+
processor_id=processor_id,
|
|
86
|
+
pod_name=self._pod_name,
|
|
87
|
+
restart_count=restart_count,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Save to backend (PVC uses the same local file path as Local)
|
|
91
|
+
if self._backend == StateBackend.CONFIGMAP:
|
|
92
|
+
await self._save_to_configmap(processor_id, state_json)
|
|
93
|
+
elif self._backend in (StateBackend.LOCAL, StateBackend.PVC):
|
|
94
|
+
await self._save_to_local(processor_id, state_json)
|
|
95
|
+
elif self._backend == StateBackend.S3:
|
|
96
|
+
await self._save_to_s3(processor_id, state_json)
|
|
97
|
+
else:
|
|
98
|
+
raise DoryStateError(f"Unsupported state backend: {self._backend}")
|
|
99
|
+
|
|
100
|
+
logger.info(f"State saved for processor {processor_id}")
|
|
101
|
+
|
|
102
|
+
async def load_state(self, processor_id: str) -> dict[str, Any] | None:
|
|
103
|
+
"""
|
|
104
|
+
Load processor state.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
processor_id: Processor ID
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
State dictionary, or None if no state found
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
DoryStateError: If load fails
|
|
114
|
+
"""
|
|
115
|
+
logger.debug(f"Loading state for processor {processor_id}")
|
|
116
|
+
|
|
117
|
+
# Load from backend
|
|
118
|
+
state_json: str | None = None
|
|
119
|
+
|
|
120
|
+
if self._backend == StateBackend.CONFIGMAP:
|
|
121
|
+
state_json = await self._load_from_configmap(processor_id)
|
|
122
|
+
elif self._backend in (StateBackend.LOCAL, StateBackend.PVC):
|
|
123
|
+
state_json = await self._load_from_local(processor_id)
|
|
124
|
+
elif self._backend == StateBackend.S3:
|
|
125
|
+
state_json = await self._load_from_s3(processor_id)
|
|
126
|
+
else:
|
|
127
|
+
raise DoryStateError(f"Unsupported state backend: {self._backend}")
|
|
128
|
+
|
|
129
|
+
if state_json is None:
|
|
130
|
+
logger.debug(f"No state found for processor {processor_id}")
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
# Deserialize state
|
|
134
|
+
state = self._serializer.deserialize(state_json)
|
|
135
|
+
logger.info(f"State loaded for processor {processor_id}")
|
|
136
|
+
return state
|
|
137
|
+
|
|
138
|
+
async def delete_state(self, processor_id: str) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
Delete processor state (golden image reset).
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
processor_id: Processor ID
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
True if state was deleted
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
DoryStateError: If delete fails
|
|
150
|
+
"""
|
|
151
|
+
logger.debug(f"Deleting state for processor {processor_id}")
|
|
152
|
+
|
|
153
|
+
if self._backend == StateBackend.CONFIGMAP:
|
|
154
|
+
return await self._delete_from_configmap(processor_id)
|
|
155
|
+
elif self._backend in (StateBackend.LOCAL, StateBackend.PVC):
|
|
156
|
+
return await self._delete_from_local(processor_id)
|
|
157
|
+
elif self._backend == StateBackend.S3:
|
|
158
|
+
return await self._delete_from_s3(processor_id)
|
|
159
|
+
else:
|
|
160
|
+
raise DoryStateError(f"Unsupported state backend: {self._backend}")
|
|
161
|
+
|
|
162
|
+
# =========================================================================
|
|
163
|
+
# ConfigMap Backend
|
|
164
|
+
# =========================================================================
|
|
165
|
+
|
|
166
|
+
def _get_configmap_store(self) -> ConfigMapStore:
|
|
167
|
+
"""Get or create ConfigMap store instance."""
|
|
168
|
+
if self._configmap_store is None:
|
|
169
|
+
self._configmap_store = ConfigMapStore(namespace=self._namespace)
|
|
170
|
+
return self._configmap_store
|
|
171
|
+
|
|
172
|
+
async def _save_to_configmap(self, processor_id: str, state_json: str) -> None:
|
|
173
|
+
"""Save state to Kubernetes ConfigMap."""
|
|
174
|
+
await self._get_configmap_store().save(processor_id, state_json)
|
|
175
|
+
|
|
176
|
+
async def _load_from_configmap(self, processor_id: str) -> str | None:
|
|
177
|
+
"""Load state from Kubernetes ConfigMap."""
|
|
178
|
+
return await self._get_configmap_store().load(processor_id)
|
|
179
|
+
|
|
180
|
+
async def _delete_from_configmap(self, processor_id: str) -> bool:
|
|
181
|
+
"""Delete state ConfigMap."""
|
|
182
|
+
return await self._get_configmap_store().delete(processor_id)
|
|
183
|
+
|
|
184
|
+
# =========================================================================
|
|
185
|
+
# Local File Backend (also used by PVC — tries /data first, then /tmp)
|
|
186
|
+
# =========================================================================
|
|
187
|
+
|
|
188
|
+
def _get_local_path(self, processor_id: str) -> Path:
|
|
189
|
+
"""Get local file path for state."""
|
|
190
|
+
# Try /data first (works when PVC is mounted), fall back to /tmp
|
|
191
|
+
base_path = Path(self.LOCAL_STATE_PATH).parent
|
|
192
|
+
if not base_path.exists():
|
|
193
|
+
base_path = Path(self.LOCAL_STATE_PATH_FALLBACK).parent
|
|
194
|
+
|
|
195
|
+
return base_path / f"dory-state-{processor_id}.json"
|
|
196
|
+
|
|
197
|
+
async def _save_to_local(self, processor_id: str, state_json: str) -> None:
|
|
198
|
+
"""Save state to local file."""
|
|
199
|
+
path = self._get_local_path(processor_id)
|
|
200
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
path.write_text(state_json)
|
|
204
|
+
logger.debug(f"State saved to local file: {path}")
|
|
205
|
+
except Exception as e:
|
|
206
|
+
raise DoryStateError(f"Failed to save state to {path}: {e}", cause=e)
|
|
207
|
+
|
|
208
|
+
async def _load_from_local(self, processor_id: str) -> str | None:
|
|
209
|
+
"""Load state from local file."""
|
|
210
|
+
path = self._get_local_path(processor_id)
|
|
211
|
+
|
|
212
|
+
if not path.exists():
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
return path.read_text()
|
|
217
|
+
except Exception as e:
|
|
218
|
+
raise DoryStateError(f"Failed to load state from {path}: {e}", cause=e)
|
|
219
|
+
|
|
220
|
+
async def _delete_from_local(self, processor_id: str) -> bool:
|
|
221
|
+
"""Delete local state file."""
|
|
222
|
+
path = self._get_local_path(processor_id)
|
|
223
|
+
|
|
224
|
+
if not path.exists():
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
path.unlink()
|
|
229
|
+
logger.debug(f"State file deleted: {path}")
|
|
230
|
+
return True
|
|
231
|
+
except Exception as e:
|
|
232
|
+
raise DoryStateError(f"Failed to delete state file {path}: {e}", cause=e)
|
|
233
|
+
|
|
234
|
+
# =========================================================================
|
|
235
|
+
# S3 Backend
|
|
236
|
+
# =========================================================================
|
|
237
|
+
|
|
238
|
+
def _get_s3_store(self) -> S3Store:
|
|
239
|
+
"""Get or create S3 store instance."""
|
|
240
|
+
if self._s3_store is None:
|
|
241
|
+
# Try to get S3 config from DoryConfig if available
|
|
242
|
+
s3_config = None
|
|
243
|
+
if self._config and hasattr(self._config, "s3_config"):
|
|
244
|
+
s3_config = self._config.s3_config
|
|
245
|
+
|
|
246
|
+
self._s3_store = S3Store(config=s3_config)
|
|
247
|
+
|
|
248
|
+
return self._s3_store
|
|
249
|
+
|
|
250
|
+
async def _save_to_s3(self, processor_id: str, state_json: str) -> None:
|
|
251
|
+
"""Save state to S3 with offline buffering support."""
|
|
252
|
+
store = self._get_s3_store()
|
|
253
|
+
await store.save(
|
|
254
|
+
processor_id,
|
|
255
|
+
state_json,
|
|
256
|
+
metadata={
|
|
257
|
+
"pod-name": self._pod_name,
|
|
258
|
+
"namespace": self._namespace,
|
|
259
|
+
},
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
async def _load_from_s3(self, processor_id: str) -> str | None:
|
|
263
|
+
"""Load state from S3 (falls back to local buffer if unavailable)."""
|
|
264
|
+
store = self._get_s3_store()
|
|
265
|
+
return await store.load(processor_id)
|
|
266
|
+
|
|
267
|
+
async def _delete_from_s3(self, processor_id: str) -> bool:
|
|
268
|
+
"""Delete state from S3."""
|
|
269
|
+
store = self._get_s3_store()
|
|
270
|
+
return await store.delete(processor_id)
|
|
271
|
+
|
|
272
|
+
async def sync_s3_buffer(self) -> int:
|
|
273
|
+
"""
|
|
274
|
+
Sync locally buffered states to S3.
|
|
275
|
+
|
|
276
|
+
Call this periodically on edge nodes to upload states
|
|
277
|
+
that were buffered during connectivity issues.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Number of states synced
|
|
281
|
+
"""
|
|
282
|
+
if self._backend != StateBackend.S3:
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
store = self._get_s3_store()
|
|
286
|
+
return await store.sync_buffer()
|