diff --git a/projects/ratchet/.editorconfig b/projects/ratchet/.editorconfig new file mode 100644 index 0000000..0a78b8f --- /dev/null +++ b/projects/ratchet/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 120 + +[*.py] +indent_size = 4 diff --git a/projects/ratchet/BUILD b/projects/ratchet/BUILD new file mode 100644 index 0000000..fe5d756 --- /dev/null +++ b/projects/ratchet/BUILD @@ -0,0 +1,9 @@ +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "lib", + srcs = glob(["src/python/**/*.py"]), + imports = ["src/python"], + deps = [ + ] +) diff --git a/projects/ratchet/LICENSE.md b/projects/ratchet/LICENSE.md new file mode 100644 index 0000000..7bcf084 --- /dev/null +++ b/projects/ratchet/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2019 Reid 'arrdem' McKenzie + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/projects/ratchet/README.md b/projects/ratchet/README.md new file mode 100644 index 0000000..1b101cc --- /dev/null +++ b/projects/ratchet/README.md @@ -0,0 +1,21 @@ +# Ratchet + +> A process that is perceived to be changing steadily in a series of irreversible steps. +> +> The unstoppable march of history; if not progress. + +Ratchet is a durable signaling mechanism. + +Ratchet provides tools for implementing _durable_ messaging, event and request/response patterns useful for implementing reliable multiprocess or distributed architectures. + +By _durable_, we mean that an acceptably performant commit log is used to record all signals and any changes to their states. + +The decision to adopt an architectural commit log such as that implemented in Ratchet enables the components of a system to be more failure oblivious and pushes the overall system towards monotone or ratcheted behavior. If state was committed prior to a failure, it can easily be recovered. If state was not committed, + +In a + +## License + +Mirrored from https://git.arrdem.com/arrdem/ratchet + +Published under the MIT license. See [LICENSE.md](LICENSE.md) diff --git a/projects/ratchet/ratchet.sqlite3 b/projects/ratchet/ratchet.sqlite3 new file mode 100644 index 0000000..a3c893b Binary files /dev/null and b/projects/ratchet/ratchet.sqlite3 differ diff --git a/projects/ratchet/setup.py b/projects/ratchet/setup.py new file mode 100644 index 0000000..ca50cb0 --- /dev/null +++ b/projects/ratchet/setup.py @@ -0,0 +1,35 @@ +from setuptools import setup + +setup( + name="arrdem.ratchet", + # Package metadata + version='0.0.0', + license="MIT", + description="A 'ratcheting' message system", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="Reid 'arrdem' McKenzie", + author_email="me@arrdem.com", + url="https://git.arrdem.com/arrdem/ratchet", + classifiers=[ + "License :: OSI Approved :: MIT License", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + ], + + # Package setup + package_dir={ + "": "src/python" + }, + packages=[ + "ratchet", + ], + entry_points={ + }, + install_requires=[ + ], + extras_require={ + } +) diff --git a/projects/ratchet/src/python/ratchet/__init__.py b/projects/ratchet/src/python/ratchet/__init__.py new file mode 100644 index 0000000..4e197e2 --- /dev/null +++ b/projects/ratchet/src/python/ratchet/__init__.py @@ -0,0 +1,123 @@ +""" +Ratchet - a 'ratcheting' messaging, queueuing and RPC system. +""" + +from abc import ABC, abstractmethod + + +class Message: + """Messages can be sent. That's it. + + Messages have headers, which may + + Other things can filter the stream of inbound messages and do log processing, but that's the whole basis of the + thing. + + """ + + +class Event: + """Events may occur. + + An event is either pending, set, or timed out. + + # Messaging protocol + + The first message states that there IS an event, and provides a global ID by which the event may be referred to and + a timeout. The initial state fo the event is `pending`. + + After the timeout has elapsed, the event MUST be DECLARED by any clients to have timed out. Attempts to set an event + after it has timed out may be recorded, but MUST be ignored and MAY NOT alter the state of the event. + + A second message MAY be sent, DECLARING that the event has occurred. This transitions the state of the event from + `pending` to `set`. + + Implementations MAY provide a message AFTER the timeout of an event occurred RECORDING that the event timed out + without being set so that listening clients may rely on a shared central clock but this is not guranteed behavior + and the timing need not be exact or transactional. + + """ + + +class Request: + """Requests may get a response. + + A request is either pending, responded, revoked or timed out. + + # Messaging protocol + + The first message states that there IS a request, provides a global ID by which the request may be referenced, a + timeout for the request and a request body. + + After the timeout has elapsed, the request MUST be DECLARED by any clients to have timed out. Attempts to deliver a + response to a request after it has timed out may be recorded, but MUST be ignored and MAY NOT alter the state of the + request. + + A second message MAY be sent, RESPONDING to to the request. This transitions the state of the request from `pending` + to `responded`. + + A second message MAY be sent, REVOKING the request. This transitions the state of the request from `pending` to + `revoked`, and MAY cause any system processing requests either to skip this one or to cancel processing the request. + Revocation of a `responded` or `timed out` request is ignored. + + """ + + +class Driver(ABC): + """Shared interface for Ratchet backend drivers.""" + + @abstractmethod + def __init__(message_ttl=60000, + message_space="_", + message_author=""): + """Initialize the driver.""" +) + @abstractmethod + def create_message(self, + message: str, + ttl: int = None, + space: str = None, + author: str = None) -> Message: + """Create a single message.""" + + @abstractmethod + def create_event(self, + timeout: int, + ttl: int = None, + space: str = None, + author: str = None): + """Create a (pending) event.""" + + @abstractmethod + def set_event(self, + timeout: int, + ttl: int = None, + space: str = None, + author: str = None): + """Attempt to mark an event as set.""" + + @abstractmethod + def create_request(self, + body: str, + timeout: int, + ttl: int = None, + space: str = None, + author: str = None): + """Create a (pending) request.""" + + @abstractmethod + def deliver_request(self, + request_id, + response: str, + ttl: int = None, + space: str = None, + author: str = None): + """Deliver a response to a (pending) request.""" + + @abstractmethod + def revoke_request(self, + request_id, + ttl: int = None, + space: str = None, + author: str = None): + """Revoke a (pending) request.""" diff --git a/projects/ratchet/src/python/ratchet/backend/sqlite.py b/projects/ratchet/src/python/ratchet/backend/sqlite.py new file mode 100644 index 0000000..efcf5bb --- /dev/null +++ b/projects/ratchet/src/python/ratchet/backend/sqlite.py @@ -0,0 +1,154 @@ +""" +An implementation of the ratchet model against SQLite. +""" + +import os +import sqlite3 as sql +from contextlib import closing +import socket + +from ratchet import Message, Event, Request + + +SCHEMA_SCRIPT = """ +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS ratchet_messages +( id INTEGER + PRIMARY KEY +, header_timestamp INTEGER + DEFAULT CURRENT_TIMESTAMP +, header_author TEXT +, header_space TEXT + DEFAULT '_' +, header_ttl INTEGER +, message TEXT +); + +CREATE TABLE IF NOT EXISTS ratchet_events +( id INTEGER + PRIMARY KEY +, header_timestamp INTEGER + DEFAULT CURRENT_TIMESTAMP +, header_author TEXT +, header_space TEXT + DEFAULT '_' +, header_ttl INTEGER +, timeout INTEGER +, state TEXT + DEFAULT 'pending' + CHECK(state IN ('pending', 'set', 'timeout')) +); + +CREATE TABLE IF NOT EXISTS ratchet_event_messages +( id INTEGER + PRIMARY KEY +, event_id INTEGER +, message_id INTEGER +, FOREIGN KEY(message_id) REFERENCES ratchet_messages(id) +, FOREIGN KEY(event_id) REFERENCES ratchet_events(id) +); + +CREATE TABLE IF NOT EXISTS ratchet_requests +( id INTEGER + PRIMARY KEY +, header_timestamp INTEGER + DEFAULT CURRENT_TIMESTAMP +, header_author TEXT +, header_space TEXT + DEFAULT '_' +, header_ttl INTEGER +, timeout INTEGER +, body TEXT +, response TEXT + DEFAULT NULL +, state TEXT + DEFAULT 'pending' + CHECK(state IN ('pending', 'responded', 'revoked', 'timeout')) +); + +CREATE TABLE IF NOT EXISTS ratchet_request_messages +( id INTEGER + PRIMARY KEY +, request_id INTEGER +, message_id INTEGER +, FOREIGN KEY(request_id) REFERENCES ratchet_requests(id) +, FOREIGN KEY(message_id) REFERENCES ratchet_events(id) +); +""" + +CREATE_MESSAGE_SCRIPT = """ +INSERT INTO ratchet_messages ( + header_author +, header_space +, header_ttl +, message +) +VALUES (?, ?, ?, ?); +""" + +CREATE_EVENT_SCRIPT = """ +INSERT INTO ratchet_events ( + header_author +, header_space +, header_ttl +, timeout +) +VALUES (?, ?, ?, ?); +""" + + + +class SQLiteDriver: + def __init__(self, + filename="~/.ratchet.sqlite3", + sqlite_timeout=1000, + message_ttl=60000, + message_space="_", + message_author=f"{os.getpid()}@{socket.gethostname()}"): + self._path = os.path.expanduser(filename) + self._sqlite_timeout = sqlite_timeout + self._message_ttl = message_ttl + self._message_space = message_space + self._message_author = message_author + + with closing(self._connection()) as conn: + self.initialize_schema(conn) + + @staticmethod + def initialize_schema(conn: sql.Connection): + conn.executescript(SCHEMA_SCRIPT) + + def _connection(self): + return sql.connect(self._filename, + timeout=self._sqlite_timeout) + + def create_message(self, + message: str, + ttl: int = None, + space: str = None, + author: str = None): + """Create a single message.""" + + ttl = ttl or self._message_ttl + space = space or self._message_space + author = author or self._message_author + with closing(self._connection()) as conn: + cursor = conn.cursor() + cursor.execute(CREATE_MESSAGE_SCRIPT, author, space, ttl, message) + return cursor.lastrowid + + def create_event(self, + timeout: int, + ttl: int = None, + space: str = None, + author: str = None): + """Create a (pending) event.""" + + ttl = ttl or self._message_ttl + space = space or self._message_space + author = author or self._message_author + with closing(self._connection()) as conn: + cursor = conn.cursor() + cursor.execute(CREATE_EVENT_SCRIPT, author, space, ttl, timeout) + return cursor.lastrowid