Skip to content

Commit

Permalink
Merge pull request #8 from resonatehq/tomas/dst-framework
Browse files Browse the repository at this point in the history
Tomas/dst framework
  • Loading branch information
Tomperez98 authored Jul 5, 2024
2 parents db12c1d + 89dc8db commit 0c98e25
Show file tree
Hide file tree
Showing 17 changed files with 399 additions and 82 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,6 @@ cython_debug/
#.idea/

# ruff
.ruff_cache/
.ruff_cache/

*.db
10 changes: 10 additions & 0 deletions examples/race-condition/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# python generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# venv
.venv
7 changes: 7 additions & 0 deletions examples/race-condition/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
3 changes: 3 additions & 0 deletions examples/race-condition/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# race-condition

Describe your project here.
61 changes: 61 additions & 0 deletions examples/race-condition/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[project]
name = "race-condition"
version = "0.1.0"
description = "Add your description here"
authors = [
{ name = "Tomperez98", email = "[email protected]" }
]
dependencies = [
"resonate-sdk-py @ file:///Users/tomasperez/Documents/resonate/resonate-sdk-py/examples/race-condition/../..",
"typing-extensions>=4.12.2",
]
readme = "README.md"
requires-python = ">= 3.10"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.rye.scripts]
check-types = "dmypy run --timeout 60 -- src tests"

[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.2.2",
"mypy>=1.10.1",
]

[tool.hatch.metadata]
allow-direct-references = true

[tool.hatch.build.targets.wheel]
packages = ["src/race_condition"]

[tool.pytest.ini_options]
testpaths = ["tests"]
xfail_strict = true
addopts = [
"--import-mode=importlib",
"--log-cli-level=DEBUG"
]

[tool.ruff.lint.per-file-ignores]
"tests/*.py" = ["INP001", "S101", "D"]

[tool.mypy]
mypy_path = "src"
plugins = []
follow_imports = "normal"
strict_optional = false
warn_redundant_casts = true
warn_unused_ignores = true
disallow_any_generics = true
check_untyped_defs = true
warn_unused_configs = true
disallow_subclassing_any = true
disallow_incomplete_defs = true
disallow_untyped_decorators = true
disallow_untyped_calls = true
disallow_untyped_defs = true
explicit_package_bases = true
32 changes: 32 additions & 0 deletions examples/race-condition/requirements-dev.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
# with-sources: false

-e file:.
exceptiongroup==1.2.1
# via pytest
iniconfig==2.0.0
# via pytest
mypy==1.10.1
mypy-extensions==1.0.0
# via mypy
packaging==24.1
# via pytest
pluggy==1.5.0
# via pytest
pytest==8.2.2
resonate-sdk-py @ file:///Users/tomasperez/Documents/resonate/resonate-sdk-py/examples/race-condition/../..
# via race-condition
result==0.17.0
# via resonate-sdk-py
tomli==2.0.1
# via mypy
# via pytest
typing-extensions==4.12.2
# via mypy
# via race-condition
16 changes: 16 additions & 0 deletions examples/race-condition/requirements.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
# with-sources: false

-e file:.
resonate-sdk-py @ file:///Users/tomasperez/Documents/resonate/resonate-sdk-py/examples/race-condition/../..
# via race-condition
result==0.17.0
# via resonate-sdk-py
typing-extensions==4.12.2
# via race-condition
69 changes: 69 additions & 0 deletions examples/race-condition/src/race_condition/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from __future__ import annotations

from sqlite3 import Connection
from typing import TYPE_CHECKING, Any

from resonate_sdk_py.scheduler.shared import Call, Yieldable

if TYPE_CHECKING:
from collections.abc import Generator

from resonate_sdk_py.context import Context


class NotEnoughMoneyError(Exception):
def __init__(self, account_id: int) -> None:
super().__init__(f"Account {account_id} does not have enough money")


def get_current_balance(ctx: Context, conn: Connection, account_id: int) -> int: # noqa: ARG001
balance: int = conn.execute(
"SELECT balance FROM accounts WHERE account_id = ?", (account_id,)
).fetchone()[0]
return balance


def modify_balance(
ctx: Context,
conn: Connection,
account_id: int,
amount: int,
) -> None:
cur = conn.execute(
"""
UPDATE accounts
SET balance = balance + ?
WHERE account_id = ?
""",
(amount, account_id),
)

ctx.assert_statement(cur.rowcount == 1, msg="More that one row was affected")


def transaction(
ctx: Context, conn: Connection, source: int, target: int, amount: int
) -> Generator[Yieldable, Any, None]:
source_balance: int = yield Call(
ctx, get_current_balance, conn=conn, account_id=source
)

if source_balance - amount < 0:
raise NotEnoughMoneyError(account_id=source)

yield Call(
ctx,
modify_balance,
conn=conn,
account_id=source,
amount=amount * -1,
)

yield Call(
ctx,
modify_balance,
conn=conn,
account_id=target,
amount=amount,
)
conn.commit()
70 changes: 70 additions & 0 deletions examples/race-condition/tests/test_race_condition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from __future__ import annotations

import sqlite3
from functools import partial
from typing import TYPE_CHECKING

import pytest
import race_condition
import resonate_sdk_py

if TYPE_CHECKING:
from collections.abc import Generator

from resonate_sdk_py.scheduler.dst import DSTScheduler


@pytest.fixture()
def setup_and_teardown() -> Generator[sqlite3.Connection, None, None]:
conn = sqlite3.connect("test.db")
ans = conn.execute("SELECT 1").fetchone()
assert ans == (1,)
conn.execute("DROP TABLE IF EXISTS accounts")
conn.execute("CREATE TABLE accounts(account_id, balance)")
conn.execute(
"""
INSERT INTO accounts VALUES
(1, 100),
(2, 0)
"""
)
conn.commit()
yield conn


@pytest.mark.parametrize("scheduler", resonate_sdk_py.testing.dst([963, 20, range(3)]))
def test_race_condition(
scheduler: DSTScheduler,
setup_and_teardown: sqlite3.Connection,
) -> None:
conn = setup_and_teardown

_ = scheduler.run(
[
partial(
race_condition.transaction,
conn=conn,
source=1,
target=2,
amount=100,
),
partial(
race_condition.transaction,
conn=conn,
source=1,
target=2,
amount=70,
),
]
)

source_balance: int = conn.execute(
"SELECT balance FROM accounts WHERE account_id = 1"
).fetchone()[0]
target_balance: int = conn.execute(
"SELECT balance FROM accounts WHERE account_id = 2"
).fetchone()[0]

assert (
source_balance == 0 and target_balance == 100
), f"Seed {scheduler.seed} causes a failure"
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dev-dependencies = [
"mypy>=1.10.0",
"pytest>=8.2.0",
"ruff>=0.4.7",
"hypothesis>=6.104.2",
]

[tool.rye.scripts]
Expand Down
6 changes: 6 additions & 0 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
# with-sources: false

-e file:.
attrs==23.2.0
# via hypothesis
exceptiongroup==1.2.1
# via hypothesis
# via pytest
hypothesis==6.104.2
iniconfig==2.0.0
# via pytest
mypy==1.10.1
Expand All @@ -23,6 +27,8 @@ pytest==8.2.2
result==0.17.0
# via resonate-sdk-py
ruff==0.5.0
sortedcontainers==2.4.0
# via hypothesis
tomli==2.0.1
# via mypy
# via pytest
Expand Down
4 changes: 2 additions & 2 deletions src/resonate_sdk_py/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import annotations

from . import testing

def hello() -> str:
return "Hello from resonate-sdk-py!"
__all__ = ["testing"]
3 changes: 1 addition & 2 deletions src/resonate_sdk_py/scheduler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
from .shared import (
Call,
CoroAndPromise,
CoroScheduler,
Invoke,
Promise,
Runnable,
Expand All @@ -42,7 +41,7 @@
P = ParamSpec("P")


class Scheduler(CoroScheduler):
class Scheduler:
def __init__(self, batch_size: int = 5, max_wokers: int | None = None) -> None:
self._stg_q = Queue[CoroAndPromise[Any]]()
self._w_thread: Thread | None = None
Expand Down
Loading

0 comments on commit 0c98e25

Please sign in to comment.