-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Design RedisSimpleQueue class (#553)
* Design RedisSimpleQueue class This class is complete with the exception of the `.put()` and `.get()` methods. `RedisSimpleQueue` will be powered by Redis streams, and the `.put()` and `.get()` methods will be implemented using `XADD` and `XREAD`/`XDEL`. https://redis.io/topics/streams-intro * Flesh out .put() and .get() methods * Make note of potential bug in redis-py * Unit test RedisSimpleQueue * Document RedisSimpleQueue * Write docstrings * Bump version number
- Loading branch information
Showing
6 changed files
with
284 additions
and
2 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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
# --------------------------------------------------------------------------- # | ||
# queue.py # | ||
# # | ||
# Copyright © 2015-2021, Rajiv Bakulesh Shah, original author. # | ||
# # | ||
# Licensed under the Apache License, Version 2.0 (the "License"); # | ||
# you may not use this file except in compliance with the License. # | ||
# You may obtain a copy of the License at: # | ||
# http://www.apache.org/licenses/LICENSE-2.0 # | ||
# # | ||
# Unless required by applicable law or agreed to in writing, software # | ||
# distributed under the License is distributed on an "AS IS" BASIS, # | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # | ||
# See the License for the specific language governing permissions and # | ||
# limitations under the License. # | ||
# --------------------------------------------------------------------------- # | ||
|
||
|
||
import math | ||
import random | ||
import time | ||
from typing import ClassVar | ||
from typing import Optional | ||
from typing import cast | ||
|
||
from redis import WatchError | ||
|
||
from .base import Base | ||
from .base import JSONTypes | ||
from .exceptions import QueueEmptyError | ||
from .timer import ContextTimer | ||
|
||
|
||
class RedisSimpleQueue(Base): | ||
RETRY_DELAY: ClassVar[int] = 200 | ||
|
||
def qsize(self) -> int: | ||
'Return the approximate size of the queue (not reliable!). O(1)' | ||
return self.redis.xlen(self.key) | ||
|
||
# Preserve the Open-Closed Principle with name mangling. | ||
# https://youtu.be/miGolgp9xq8?t=2086 | ||
# https://stackoverflow.com/a/38534939 | ||
__qsize = qsize | ||
|
||
def empty(self) -> bool: | ||
'Return True if the queue is empty, False otherwise (not reliable!). O(1)' | ||
return self.__qsize() == 0 | ||
|
||
def put(self, | ||
item: JSONTypes, | ||
block: bool = True, | ||
timeout: Optional[float] = None, | ||
) -> None: | ||
'''Put the item on the queue. O(1) | ||
The optional 'block' and 'timeout' arguments are ignored, as this method | ||
never blocks. They are provided for compatibility with the queue.Queue | ||
class. | ||
''' | ||
encoded_value = self._encode(item) | ||
self.redis.xadd(self.key, {'item': encoded_value}, id='*') | ||
|
||
__put = put | ||
|
||
def put_nowait(self, item: JSONTypes) -> None: | ||
'''Put an item into the queue without blocking. O(1) | ||
This is exactly equivalent to `.put(item)` and is only provided for | ||
compatibility with the queue.Queue class. | ||
''' | ||
return self.__put(item, False) | ||
|
||
def get(self, | ||
block: bool = True, | ||
timeout: Optional[float] = None, | ||
) -> JSONTypes: | ||
'''Remove and return an item from the queue. O(1) | ||
If optional args 'block' is true and 'timeout' is None (the default), | ||
block if necessary until an item is available. If 'timeout' is | ||
a non-negative number, it blocks at most 'timeout' seconds and raises | ||
the Empty exception if no item was available within that time. | ||
Otherwise ('block' is false), return an item if one is immediately | ||
available, else raise the QueueEmptyError exception ('timeout' is | ||
ignored in that case). | ||
''' | ||
redis_block = (timeout or 0.0) if block else 0.0 | ||
redis_block = math.floor(redis_block) | ||
with ContextTimer() as timer: | ||
while True: | ||
try: | ||
item = self.__remove_and_return(redis_block) | ||
return item | ||
except (WatchError, IndexError): | ||
if not block or timer.elapsed() / 1000 >= (timeout or 0): | ||
raise QueueEmptyError(redis=self.redis, key=self.key) | ||
delay = random.uniform(0, self.RETRY_DELAY/1000) | ||
time.sleep(delay) | ||
|
||
__get = get | ||
|
||
def __remove_and_return(self, redis_block: int) -> JSONTypes: | ||
with self._watch() as pipeline: | ||
# XXX: The following line raises WatchError after the socket timeout | ||
# if the RedisQueue is empty and we're not blocking. This feels | ||
# like a bug in redis-py? | ||
returned_value = pipeline.xread({self.key: 0}, count=1, block=redis_block) | ||
# The following line raises IndexError if the RedisQueue is empty | ||
# and we're blocking. | ||
id_ = cast(bytes, returned_value[0][1][0][0]) | ||
dict_ = cast(dict, returned_value[0][1][0][1]) | ||
pipeline.multi() | ||
pipeline.xdel(self.key, id_) | ||
encoded_value = dict_[b'item'] | ||
item = self._decode(encoded_value) | ||
return item | ||
|
||
def get_nowait(self) -> JSONTypes: | ||
'''Remove and return an item from the queue without blocking. O(1) | ||
Only get an item if one is immediately available. Otherwise | ||
raise the Empty exception. | ||
''' | ||
return self.__get(False) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
# --------------------------------------------------------------------------- # | ||
# test_queue.py # | ||
# # | ||
# Copyright © 2015-2021, Rajiv Bakulesh Shah, original author. # | ||
# # | ||
# Licensed under the Apache License, Version 2.0 (the "License"); # | ||
# you may not use this file except in compliance with the License. # | ||
# You may obtain a copy of the License at: # | ||
# http://www.apache.org/licenses/LICENSE-2.0 # | ||
# # | ||
# Unless required by applicable law or agreed to in writing, software # | ||
# distributed under the License is distributed on an "AS IS" BASIS, # | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # | ||
# See the License for the specific language governing permissions and # | ||
# limitations under the License. # | ||
# --------------------------------------------------------------------------- # | ||
|
||
|
||
from pottery import ContextTimer | ||
from pottery import QueueEmptyError | ||
from pottery import RedisSimpleQueue | ||
from tests.base import TestCase | ||
|
||
|
||
class QueueTests(TestCase): | ||
def test_put(self): | ||
queue = RedisSimpleQueue() | ||
|
||
assert queue.qsize() == 0 | ||
assert queue.empty() | ||
|
||
for num in range(1, 6): | ||
with self.subTest(num=num): | ||
queue.put(num) | ||
assert queue.qsize() == num | ||
assert not queue.empty() | ||
|
||
def test_put_nowait(self): | ||
queue = RedisSimpleQueue() | ||
|
||
assert queue.qsize() == 0 | ||
assert queue.empty() | ||
|
||
for num in range(1, 6): | ||
with self.subTest(num=num): | ||
queue.put_nowait(num) | ||
assert queue.qsize() == num | ||
assert not queue.empty() | ||
|
||
def test_get(self): | ||
queue = RedisSimpleQueue() | ||
|
||
with self.assertRaises(QueueEmptyError): | ||
queue.get() | ||
|
||
for num in range(1, 6): | ||
with self.subTest(num=num): | ||
queue.put(num) | ||
assert queue.get() == num | ||
assert queue.qsize() == 0 | ||
assert queue.empty() | ||
|
||
with self.assertRaises(QueueEmptyError): | ||
queue.get() | ||
|
||
def test_get_nowait(self): | ||
queue = RedisSimpleQueue() | ||
|
||
with self.assertRaises(QueueEmptyError): | ||
queue.get_nowait() | ||
|
||
for num in range(1, 6): | ||
queue.put(num) | ||
|
||
for num in range(1, 6): | ||
with self.subTest(num=num): | ||
assert queue.get_nowait() == num | ||
assert queue.qsize() == 5 - num | ||
assert queue.empty() == (num == 5) | ||
|
||
with self.assertRaises(QueueEmptyError): | ||
queue.get_nowait() | ||
|
||
def test_get_timeout(self): | ||
queue = RedisSimpleQueue() | ||
timeout = 1 | ||
|
||
with self.assertRaises(QueueEmptyError), ContextTimer() as timer: | ||
queue.get(timeout=timeout) | ||
assert timer.elapsed() / 1000 >= timeout |