Import the Ratchet project

This commit is contained in:
Reid 'arrdem' McKenzie 2021-04-09 01:48:40 -06:00
parent bf22e072e3
commit d3192702fc
8 changed files with 361 additions and 0 deletions

View file

@ -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

9
projects/ratchet/BUILD Normal file
View file

@ -0,0 +1,9 @@
package(default_visibility = ["//visibility:public"])
py_library(
name = "lib",
srcs = glob(["src/python/**/*.py"]),
imports = ["src/python"],
deps = [
]
)

View file

@ -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.

View file

@ -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)

Binary file not shown.

35
projects/ratchet/setup.py Normal file
View file

@ -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={
}
)

View file

@ -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."""

View file

@ -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