Skip to content

Commit 95c5453

Browse files
committed
refactor(quix_configuration_service): treat valid_from=0 as always valid
1 parent 24bedb6 commit 95c5453

File tree

2 files changed

+36
-48
lines changed

2 files changed

+36
-48
lines changed

quixstreams/dataframe/joins/lookups/quix_configuration_service/models.py

Lines changed: 30 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ class Field(BaseField):
7474
def __post_init__(self) -> None:
7575
"""
7676
Compile the JSONPath expression after initialization.
77+
78+
This method is called automatically after the dataclass is initialized to ensure that the JSONPath expression is compiled and ready for use.
79+
Since the dataclass is frozen, we cannot modify its attributes directly in the constructor and must use `__setattr__` to set the `_jsonpath` attribute.
7780
"""
7881
super().__setattr__("_jsonpath", parse(self.jsonpath))
7982

@@ -121,6 +124,11 @@ class ConfigurationVersion:
121124
"""
122125
Represents a specific version of a configuration.
123126
127+
This class is designed to be immutable (frozen) and hashable so it can be safely used as a key in an LRU cache.
128+
The `retry_count` and `retry_at` attributes are intentionally excluded from the hash calculation and immutability,
129+
because they are mutable fields used for tracking API retry logic. These fields are not relevant for caching or equality,
130+
and should be updated by calling `__setattr__` directly, since the dataclass is otherwise frozen.
131+
124132
:param id: The configuration ID.
125133
:param version: The version number.
126134
:param contentUrl: URL to fetch the configuration content.
@@ -132,7 +140,7 @@ class ConfigurationVersion:
132140
version: int
133141
contentUrl: str
134142
sha256sum: str
135-
valid_from: Optional[float] # timestamp ms
143+
valid_from: float # timestamp ms
136144
retry_count: int = dataclasses.field(default=0, hash=False, init=False)
137145
retry_at: int = dataclasses.field(default=sys.maxsize, hash=False, init=False)
138146

@@ -148,7 +156,7 @@ def from_event(cls, event: Event) -> "ConfigurationVersion":
148156

149157
raw_valid_from = event["metadata"]["valid_from"]
150158
if raw_valid_from is None:
151-
valid_from: Optional[float] = None
159+
valid_from: float = 0
152160
else:
153161
# TODO python 3.11: Use `datetime.fromisoformat` when additional format are available
154162
try:
@@ -251,37 +259,21 @@ def find_valid_version(self, timestamp: int) -> Optional[ConfigurationVersion]:
251259

252260
# Check if the next version has become valid (timestamp has moved forward)
253261
# If so, recalculate all cached versions
254-
if self.next_version:
255-
# If next version has no valid_from date, it's always valid
256-
if self.next_version.valid_from is None:
257-
self.previous_version, self.version, self.next_version = (
258-
self._find_versions(timestamp)
259-
)
260-
return self.version
261-
# If next version's valid_from is before or at the timestamp, it's valid
262-
if self.next_version.valid_from <= timestamp:
263-
self.previous_version, self.version, self.next_version = (
264-
self._find_versions(timestamp)
265-
)
266-
return self.version
262+
if self.next_version and self.next_version.valid_from <= timestamp:
263+
self.previous_version, self.version, self.next_version = (
264+
self._find_versions(timestamp)
265+
)
266+
return self.version
267267

268268
# Check if the current cached version is still valid for this timestamp
269-
# If version has no valid_from date, it's always valid
270-
if self.version.valid_from is None:
271-
return self.version
272269
# If version's valid_from is before or at the timestamp, it's valid
273270
if self.version.valid_from <= timestamp:
274271
return self.version
275272

276273
# Fallback: check if the previous version is valid for this timestamp
277274
# This can happen when messages are out of order and timestamp is before the current version's valid_from
278-
if self.previous_version:
279-
# If previous version has no valid_from date, it's always valid
280-
if self.previous_version.valid_from is None:
281-
return self.previous_version
282-
# If previous version's valid_from is before or at the timestamp, it's valid
283-
if self.previous_version.valid_from <= timestamp:
284-
return self.previous_version
275+
if self.previous_version and self.previous_version.valid_from <= timestamp:
276+
return self.previous_version
285277

286278
# If cached versions don't match, recalculate all versions for this timestamp
287279
self.previous_version, self.version, self.next_version = self._find_versions(
@@ -317,35 +309,31 @@ def _find_versions(
317309
for _, version in sorted(
318310
self.versions.items(), reverse=True, key=lambda x: x[0]
319311
):
320-
# Handle versions with no valid_from timestamp (always valid)
321-
if version.valid_from is None:
322-
# First version with no valid_from becomes the current version
323-
if current_version is None:
324-
current_version = version
325-
return previous_version, current_version, next_version
326-
# Second version with no valid_from becomes the previous version
327-
elif previous_version is None:
328-
previous_version = version
329-
# Early return since we have current and previous, no next needed for no-timestamp versions
330-
return previous_version, current_version, next_version
331-
332312
# Handle versions that are valid in the future (after the timestamp)
333-
elif version.valid_from > timestamp:
313+
if version.valid_from > timestamp:
314+
# If we already have a current version, if has a higher version number,
315+
# so we can skip all future versions
316+
if current_version is not None:
317+
continue
318+
334319
# First future version becomes the next version
335320
if next_version is None:
336321
next_version = version
337322
# If we find an earlier future version, it becomes the new next version
338-
elif (
339-
next_version.valid_from is not None
340-
and version.valid_from < next_version.valid_from
341-
):
323+
elif version.valid_from < next_version.valid_from:
342324
next_version = version
343325

344326
# Handle versions that are valid at or before the timestamp
345327
else: # version.valid_from <= timestamp
346328
# First valid version becomes the current version
347329
if current_version is None:
348330
current_version = version
331+
# We can short circuit if we find a version that is valid always valid.
332+
# There is no need for a previous_version as the current is valid from the beginning.
333+
# There is no need to look for a next_version either, it will have a lower version number
334+
# since the loop is ordered by version number.
335+
if current_version.valid_from == 0.0:
336+
return previous_version, current_version, next_version
349337
# Second valid version becomes the previous version
350338
elif previous_version is None:
351339
previous_version = version

tests/test_quixstreams/test_dataframe/test_joins/test_lookup_quix_config.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def lookup():
4545
def create_configuration_version(
4646
id: str = "test-config",
4747
version: int = 1,
48-
valid_from: Optional[float] = None,
48+
valid_from: float = 0,
4949
sha256sum: str = "test-hash",
5050
content_url: str = "http://example.com/config",
5151
) -> ConfigurationVersion:
@@ -147,7 +147,7 @@ def test_find_valid_version_empty_versions(self):
147147

148148
def test_find_valid_version_single_version_no_timestamp(self):
149149
"""Test finding valid version with a single version that has no valid_from timestamp."""
150-
version = create_configuration_version(version=1, valid_from=None)
150+
version = create_configuration_version(version=1, valid_from=0)
151151
config = Configuration(versions={1: version})
152152

153153
result = config.find_valid_version(1500)
@@ -259,7 +259,7 @@ def test_find_valid_version_mixed_timestamp_and_no_timestamp(self):
259259
"""Test finding valid version with mix of timestamped and non-timestamped versions."""
260260
version1 = create_configuration_version(version=1, valid_from=1000.0)
261261
version2 = create_configuration_version(
262-
version=2, valid_from=None
262+
version=2, valid_from=0.0
263263
) # No timestamp
264264
version3 = create_configuration_version(version=3, valid_from=3000.0)
265265

@@ -316,8 +316,8 @@ def test_find_versions_internal_helper(self):
316316

317317
def test_find_versions_with_no_timestamp_versions(self):
318318
"""Test _find_versions with versions that have no valid_from timestamp."""
319-
version1 = create_configuration_version(version=1, valid_from=None)
320-
version2 = create_configuration_version(version=2, valid_from=None)
319+
version1 = create_configuration_version(version=1, valid_from=0.0)
320+
version2 = create_configuration_version(version=2, valid_from=0.0)
321321

322322
config = Configuration(versions={1: version1, 2: version2})
323323

@@ -407,7 +407,7 @@ def test_from_event_without_timestamp(self) -> None:
407407

408408
assert version.id == "test-config"
409409
assert version.version == 1
410-
assert version.valid_from is None
410+
assert version.valid_from == 0
411411

412412
def test_success_method(self):
413413
"""Test the success method resets retry parameters."""

0 commit comments

Comments
 (0)