-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added
OperationsQueue
component (#7)
- Loading branch information
Showing
7 changed files
with
223 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
from __future__ import annotations | ||
|
||
from abc import ( | ||
ABC, | ||
abstractmethod, | ||
) | ||
from types import TracebackType | ||
|
||
|
||
class AutoCloseable(ABC): | ||
def __enter__(self) -> AutoCloseable: | ||
return self | ||
|
||
@abstractmethod | ||
def close(self) -> None: ... | ||
|
||
def __exit__( | ||
self, | ||
exc_type: type[BaseException] | None, | ||
exc_value: BaseException | None, | ||
traceback: TracebackType | None, | ||
) -> None: | ||
self.close() | ||
|
||
|
||
class Resource(AutoCloseable): | ||
@abstractmethod | ||
def cleanup(self) -> None: ... | ||
|
||
def flush(self) -> None: | ||
pass | ||
|
||
def close(self) -> None: | ||
self.flush() | ||
|
||
|
||
class WithResources(Resource): | ||
@property | ||
@abstractmethod | ||
def resources(self) -> tuple[Resource, ...]: ... | ||
|
||
def flush(self) -> None: | ||
for resource in self.resources: | ||
resource.flush() | ||
|
||
def close(self) -> None: | ||
for resource in self.resources: | ||
resource.close() | ||
|
||
def cleanup(self) -> None: | ||
for resource in self.resources: | ||
resource.cleanup() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
from __future__ import annotations | ||
|
||
__all__ = ("OperationsQueue",) | ||
|
||
from multiprocessing import Queue | ||
from time import monotonic | ||
from typing import ( | ||
TYPE_CHECKING, | ||
Callable, | ||
NamedTuple, | ||
) | ||
|
||
from neptune_scale.core.components.abstract import Resource | ||
from neptune_scale.core.validation import verify_type | ||
from neptune_scale.parameters import MAX_QUEUE_ELEMENT_SIZE | ||
|
||
if TYPE_CHECKING: | ||
from threading import RLock | ||
|
||
from neptune_api.proto.neptune_pb.ingest.v1.pub.ingest_pb2 import RunOperation | ||
|
||
|
||
class QueueElement(NamedTuple): | ||
sequence_id: int | ||
occured_at: float | ||
operation: bytes | ||
|
||
|
||
def default_max_size_exceeded_callback(max_size: int, e: BaseException) -> None: | ||
raise ValueError(f"Queue is full (max size: {max_size})") from e | ||
|
||
|
||
class OperationsQueue(Resource): | ||
def __init__( | ||
self, | ||
*, | ||
lock: RLock, | ||
max_size: int = 0, | ||
max_size_exceeded_callback: Callable[[int, BaseException], None] | None = None, | ||
) -> None: | ||
verify_type("max_size", max_size, int) | ||
|
||
self._lock: RLock = lock | ||
self._max_size: int = max_size | ||
self._max_size_exceeded_callback: Callable[[int, BaseException], None] = ( | ||
max_size_exceeded_callback if max_size_exceeded_callback is not None else default_max_size_exceeded_callback | ||
) | ||
|
||
self._sequence_id: int = 0 | ||
self._queue: Queue[QueueElement] = Queue(maxsize=max_size) | ||
|
||
def enqueue(self, *, operation: RunOperation) -> None: | ||
try: | ||
with self._lock: | ||
serialized_operation = operation.SerializeToString() | ||
|
||
if len(serialized_operation) > MAX_QUEUE_ELEMENT_SIZE: | ||
raise ValueError(f"Operation size exceeds the maximum allowed size ({MAX_QUEUE_ELEMENT_SIZE})") | ||
|
||
self._queue.put_nowait(QueueElement(self._sequence_id, monotonic(), serialized_operation)) | ||
self._sequence_id += 1 | ||
except Exception as e: | ||
self._max_size_exceeded_callback(self._max_size, e) | ||
|
||
def cleanup(self) -> None: | ||
pass | ||
|
||
def close(self) -> None: | ||
self._queue.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
MAX_RUN_ID_LENGTH = 128 | ||
MAX_FAMILY_LENGTH = 128 | ||
MAX_QUEUE_SIZE = 32767 | ||
MAX_QUEUE_ELEMENT_SIZE = 1024 * 1024 # 1MB |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import threading | ||
from unittest.mock import MagicMock | ||
|
||
import pytest | ||
from neptune_api.proto.neptune_pb.ingest.v1.common_pb2 import ( | ||
UpdateRunSnapshot, | ||
Value, | ||
) | ||
from neptune_api.proto.neptune_pb.ingest.v1.pub.ingest_pb2 import RunOperation | ||
|
||
from neptune_scale.core.components.operations_queue import OperationsQueue | ||
|
||
|
||
def test__enqueue(): | ||
# given | ||
lock = threading.RLock() | ||
queue = OperationsQueue(lock=lock, max_size=0) | ||
|
||
# and | ||
operation = RunOperation() | ||
|
||
# when | ||
queue.enqueue(operation=operation) | ||
|
||
# then | ||
assert queue._sequence_id == 1 | ||
|
||
# when | ||
queue.enqueue(operation=operation) | ||
|
||
# then | ||
assert queue._sequence_id == 2 | ||
|
||
|
||
def test__max_queue_size_exceeded(): | ||
# given | ||
lock = threading.RLock() | ||
callback = MagicMock() | ||
queue = OperationsQueue(lock=lock, max_size=1, max_size_exceeded_callback=callback) | ||
|
||
# and | ||
operation = RunOperation() | ||
|
||
# when | ||
queue.enqueue(operation=operation) | ||
queue.enqueue(operation=operation) | ||
|
||
# then | ||
callback.assert_called_once() | ||
|
||
|
||
def test__max_element_size_exceeded(): | ||
# given | ||
lock = threading.RLock() | ||
queue = OperationsQueue(lock=lock, max_size=1) | ||
|
||
# and | ||
snapshot = UpdateRunSnapshot(assign={f"key_{i}": Value(string=("a" * 1024)) for i in range(1024)}) | ||
operation = RunOperation(update=snapshot) | ||
|
||
# then | ||
with pytest.raises(ValueError): | ||
queue.enqueue(operation=operation) |