Skip to content

Commit

Permalink
Add some documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathangreen committed Jun 11, 2024
1 parent 4db2ea5 commit 7af6e1c
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 17 deletions.
69 changes: 53 additions & 16 deletions src/palace/manager/service/redis/models/patron_activity.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import datetime
from enum import IntEnum
import sys
from functools import cached_property
from types import TracebackType
from typing import Any, Literal, NamedTuple
Expand All @@ -13,6 +13,12 @@
from palace.manager.util.datetime_helpers import utc_now
from palace.manager.util.log import LoggerMixin

# TODO: Remove this when we drop support for Python 3.10
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from backports.strenum import StrEnum


class PatronActivityError(BasePalaceException, RuntimeError):
...
Expand All @@ -21,29 +27,44 @@ class PatronActivityError(BasePalaceException, RuntimeError):
class PatronActivityStatus:
"""
Serialize and deserialize the status of a patron activity sync task from a string
stored in Redis. The string formatted using a fixed length for each field, separated
by a double colon.
stored in Redis. The string consists of three fields separated by cls.SEPERATOR:
- The state of the task.
- The timestamp of the task.
- The task id.
The state is a one character string representing the state of the task. The timestamp
is a 19 character string representing the time the task was last updated in UTC. It is
stored in ISO 8601 format with second precision without a timezone. The task id is a
string that uniquely identifies the task.
"""

class State(IntEnum):
"""The state of a patron activity sync."""
class State(StrEnum):
"""
The state of a patron activity sync.
The value of the enum is what gets stored in redis for the state.
"""

# The task is currently running.
LOCKED = 1
LOCKED = "L"
# The task failed to complete.
FAILED = 5
FAILED = "F"
# The task completed successfully.
SUCCESS = 6
SUCCESS = "S"
# The api does not support patron activity sync.
NOT_SUPPORTED = 9

def __str__(self) -> str:
return f"{self.value:02}"
NOT_SUPPORTED = "N"

class _FieldOffset(NamedTuple):
"""
A class to manage the offsets of fields in the redis string representation of the
patron activity status.
A class to manage the offsets of fields in the redis string representation.
This helper is here because when we slice a string in Python, the end index is
exclusive. However, when we use the redis GETRANGE command, the end index is
inclusive. This class helps us manage the difference between the two.
The start index is inclusive and the end index is exclusive, as in Python. The
redis_end property returns the end index in the format expected by the GETRANGE
command.
"""

start: int
Expand All @@ -55,13 +76,20 @@ def slice(self) -> slice:

@property
def redis_end(self) -> int:
"""
Get the end index in the format expected by redis GETRANGE.
"""
return self.end - 1 if self.end is not None else -1

@property
def redis(self) -> str:
"""
Get the start and end index as a string, that can be directly used in a redis
GETRANGE command.
"""
return f"{self.start}, {self.redis_end}"

STATE_FIELD_LEN = 2
STATE_FIELD_LEN = 1
TIMESTAMP_FIELD_LEN = 19

SEPERATOR = "::"
Expand Down Expand Up @@ -100,7 +128,7 @@ def from_redis(cls, data: str) -> Self:
task_id = data[cls.TASK_ID_OFFSET.slice]

return cls(
state=cls.State(int(state)),
state=cls.State(state),
task_id=task_id,
timestamp=aware_timestamp,
)
Expand Down Expand Up @@ -140,6 +168,15 @@ def __eq__(self, other: Any) -> bool:
class PatronActivity(LoggerMixin):
"""
A class to manage the status of a patron activity sync in Redis.
It provides a locking mechanism, so only one task updates the patron activity at a time,
it also provides a state, so we know if the task is running, failed, succeeded, or not supported.
Each state change stores a timestamp, so we know when the task was last updated.
Each status is stored in redis with a timeout, so that eventually, the status will be cleared.
The design of the locking mechanism is inspired by the Redis documentation on distributed locks:
https://redis.io/docs/latest/develop/use/patterns/distributed-locks/#correct-implementation-with-a-single-instance
"""

LOCKED_TIMEOUT = 60 * 15 # 15 minutes
Expand Down
2 changes: 1 addition & 1 deletion tests/manager/service/redis/models/test_patron_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def test_to_redis(self):
with pytest.raises(ValueError):
status.to_redis()

assert status.to_redis() == "05::2024-06-11T01:24:00::test-123"
assert status.to_redis() == "F::2024-06-11T01:24:00::test-123"


class PatronActivityFixture:
Expand Down

0 comments on commit 7af6e1c

Please sign in to comment.