Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python: ['3.10', '3.11', '3.12']
python: ['3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python }}
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "dispatch-py"
description = "Develop reliable distributed systems on the Dispatch platform."
readme = "README.md"
dynamic = ["version"]
requires-python = ">= 3.10"
requires-python = ">= 3.9"
dependencies = [
"grpcio >= 1.60.0",
"protobuf >= 4.24.0",
Expand All @@ -17,7 +17,8 @@ dependencies = [
"tblib >= 3.0.0",
"docopt >= 0.6.2",
"types-docopt >= 0.6.11.4",
"httpx >= 0.27.0"
"httpx >= 0.27.0",
"typing_extensions >= 4.10"
]

[project.optional-dependencies]
Expand Down
6 changes: 3 additions & 3 deletions src/dispatch/coroutine.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ def race(*awaitables: Awaitable[Any]) -> list[Any]: # type: ignore[misc]
return (yield RaceDirective(awaitables))


@dataclass(slots=True)
@dataclass
class AllDirective:
awaitables: tuple[Awaitable[Any], ...]


@dataclass(slots=True)
@dataclass
class AnyDirective:
awaitables: tuple[Awaitable[Any], ...]


@dataclass(slots=True)
@dataclass
class RaceDirective:
awaitables: tuple[Awaitable[Any], ...]

Expand Down
18 changes: 11 additions & 7 deletions src/dispatch/experimental/durable/frame.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>

#if PY_MAJOR_VERSION != 3 || (PY_MINOR_VERSION < 10 || PY_MINOR_VERSION > 13)
# error Python 3.10-3.13 is required
#if PY_MAJOR_VERSION != 3 || (PY_MINOR_VERSION < 9 || PY_MINOR_VERSION > 13)
# error Python 3.9-3.13 is required
#endif

// This is a redefinition of the private PyTryBlock from 3.10.
// This is a redefinition of the private PyTryBlock from <= 3.10.
// https://github.com/python/cpython/blob/3.9/Include/cpython/frameobject.h#L11
// https://github.com/python/cpython/blob/3.10/Include/cpython/frameobject.h#L22
typedef struct {
int b_type;
int b_handler;
int b_level;
} PyTryBlock;

// This is a redefinition of the private PyCoroWrapper from 3.10-3.13.
// This is a redefinition of the private PyCoroWrapper from 3.9-3.13.
// https://github.com/python/cpython/blob/3.9/Objects/genobject.c#L830
// https://github.com/python/cpython/blob/3.10/Objects/genobject.c#L884
// https://github.com/python/cpython/blob/3.11/Objects/genobject.c#L1016
// https://github.com/python/cpython/blob/3.12/Objects/genobject.c#L1003
Expand Down Expand Up @@ -51,7 +53,9 @@ static int get_frame_iblock(Frame *frame);
static void set_frame_iblock(Frame *frame, int iblock);
static PyTryBlock *get_frame_blockstack(Frame *frame);

#if PY_MINOR_VERSION == 10
#if PY_MINOR_VERSION == 9
#include "frame309.h"
#elif PY_MINOR_VERSION == 10
#include "frame310.h"
#elif PY_MINOR_VERSION == 11
#include "frame311.h"
Expand All @@ -78,7 +82,7 @@ static const char *get_type_name(PyObject *obj) {

static PyGenObject *get_generator_like_object(PyObject *obj) {
if (PyGen_Check(obj) || PyCoro_CheckExact(obj) || PyAsyncGen_CheckExact(obj)) {
// Note: In Python 3.10-3.13, the PyGenObject, PyCoroObject and PyAsyncGenObject
// Note: In Python 3.9-3.13, the PyGenObject, PyCoroObject and PyAsyncGenObject
// have the same layout, they just have different field prefixes (gi_, cr_, ag_).
// We cast to PyGenObject here so that the remainder of the code can use the gi_
// prefix for all three cases.
Expand Down Expand Up @@ -386,7 +390,7 @@ static PyObject *ext_set_frame_stack_at(PyObject *self, PyObject *args) {
}
PyObject **localsplus = get_frame_localsplus(frame);
PyObject *prev = localsplus[index];
if (Py_IsTrue(unset)) {
if (PyObject_IsTrue(unset)) {
localsplus[index] = NULL;
} else {
Py_INCREF(stack_obj);
Expand Down
32 changes: 19 additions & 13 deletions src/dispatch/experimental/durable/frame.pyi
Original file line number Diff line number Diff line change
@@ -1,55 +1,61 @@
from types import FrameType
from typing import Any, AsyncGenerator, Coroutine, Generator, Tuple
from typing import Any, AsyncGenerator, Coroutine, Generator, Tuple, Union

def get_frame_ip(frame: FrameType | Coroutine | Generator | AsyncGenerator) -> int:
def get_frame_ip(frame: Union[FrameType, Coroutine, Generator, AsyncGenerator]) -> int:
"""Get instruction pointer of a generator or coroutine."""

def set_frame_ip(frame: FrameType | Coroutine | Generator | AsyncGenerator, ip: int):
def set_frame_ip(
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator], ip: int
):
"""Set instruction pointer of a generator or coroutine."""

def get_frame_sp(frame: FrameType | Coroutine | Generator | AsyncGenerator) -> int:
def get_frame_sp(frame: Union[FrameType, Coroutine, Generator, AsyncGenerator]) -> int:
"""Get stack pointer of a generator or coroutine."""

def set_frame_sp(frame: FrameType | Coroutine | Generator | AsyncGenerator, sp: int):
def set_frame_sp(
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator], sp: int
):
"""Set stack pointer of a generator or coroutine."""

def get_frame_bp(frame: FrameType | Coroutine | Generator | AsyncGenerator) -> int:
def get_frame_bp(frame: Union[FrameType, Coroutine, Generator, AsyncGenerator]) -> int:
"""Get block pointer of a generator or coroutine."""

def set_frame_bp(frame: FrameType | Coroutine | Generator | AsyncGenerator, bp: int):
def set_frame_bp(
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator], bp: int
):
"""Set block pointer of a generator or coroutine."""

def get_frame_stack_at(
frame: FrameType | Coroutine | Generator | AsyncGenerator, index: int
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator], index: int
) -> Tuple[bool, Any]:
"""Get an object from a generator or coroutine's stack, as an (is_null, obj) tuple."""

def set_frame_stack_at(
frame: FrameType | Coroutine | Generator | AsyncGenerator,
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator],
index: int,
unset: bool,
value: Any,
):
"""Set or unset an object on the stack of a generator or coroutine."""

def get_frame_block_at(
frame: FrameType | Coroutine | Generator | AsyncGenerator, index: int
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator], index: int
) -> Tuple[int, int, int]:
"""Get a block from a generator or coroutine."""

def set_frame_block_at(
frame: FrameType | Coroutine | Generator | AsyncGenerator,
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator],
index: int,
value: Tuple[int, int, int],
):
"""Restore a block of a generator or coroutine."""

def get_frame_state(
frame: FrameType | Coroutine | Generator | AsyncGenerator,
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator],
) -> int:
"""Get frame state of a generator or coroutine."""

def set_frame_state(
frame: FrameType | Coroutine | Generator | AsyncGenerator, state: int
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator], state: int
):
"""Set frame state of a generator or coroutine."""
144 changes: 144 additions & 0 deletions src/dispatch/experimental/durable/frame309.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// This is a redefinition of the private/opaque frame object.
// https://github.com/python/cpython/blob/3.9/Include/cpython/frameobject.h#L17
//
// In Python <= 3.10, `struct _frame` is both the PyFrameObject and
// PyInterpreterFrame. From Python 3.11 onwards, the two were split with the
// PyFrameObject (struct _frame) pointing to struct _PyInterpreterFrame.
struct Frame {
PyObject_VAR_HEAD
struct Frame *f_back; // struct _frame
PyCodeObject *f_code;
PyObject *f_builtins;
PyObject *f_globals;
PyObject *f_locals;
PyObject **f_valuestack;
PyObject **f_stacktop;
PyObject *f_trace;
char f_trace_lines;
char f_trace_opcodes;
PyObject *f_gen;
int f_lasti;
int f_lineno;
int f_iblock;
char f_executing;
PyTryBlock f_blockstack[CO_MAXBLOCKS];
PyObject *f_localsplus[1];
};

// Python 3.9 and prior didn't have an explicit enum of frame states,
// but we can derive them based on the presence of a frame, and other
// information found on the frame, for compatibility with later versions.
typedef enum _framestate {
FRAME_CREATED = -2,
FRAME_EXECUTING = 0,
FRAME_CLEARED = 4
} FrameState;

/*
// This is the definition of PyGenObject for reference to developers
// working on the extension.
//
// Note that PyCoroObject and PyAsyncGenObject have the same layout as
// PyGenObject, however the struct fields have a cr_ and ag_ prefix
// (respectively) rather than a gi_ prefix. In Python <= 3.10, PyCoroObject
// and PyAsyncGenObject have extra fields compared to PyGenObject. In Python
// 3.11 onwards, the three objects are identical (except for field name
// prefixes). The extra fields in Python <= 3.10 are not applicable to the
// extension at this time.
//
// https://github.com/python/cpython/blob/3.9/Include/genobject.h#L15
typedef struct {
PyObject_HEAD
PyFrameObject *gi_frame;
char gi_running;
PyObject *gi_code;
PyObject *gi_weakreflist;
PyObject *gi_name;
PyObject *gi_qualname;
_PyErr_StackItem gi_exc_state;
} PyGenObject;
*/

static Frame *get_frame(PyGenObject *gen_like) {
Frame *frame = (Frame *)(gen_like->gi_frame);
assert(frame);
return frame;
}

static PyCodeObject *get_frame_code(Frame *frame) {
PyCodeObject *code = frame->f_code;
assert(code);
return code;
}

static int get_frame_lasti(Frame *frame) {
return frame->f_lasti;
}

static void set_frame_lasti(Frame *frame, int lasti) {
frame->f_lasti = lasti;
}

static int get_frame_state(PyGenObject *gen_like) {
// Python 3.9 doesn't have frame states, but we can derive
// some for compatibility with later versions and to simplify
// the extension.
Frame *frame = (Frame *)(gen_like->gi_frame);
if (!frame) {
return FRAME_CLEARED;
}
return frame->f_executing ? FRAME_EXECUTING : FRAME_CREATED;
}

static void set_frame_state(PyGenObject *gen_like, int fs) {
Frame *frame = get_frame(gen_like);
frame->f_executing = (fs == FRAME_EXECUTING);
}

static int valid_frame_state(int fs) {
return fs == FRAME_CREATED || fs == FRAME_EXECUTING || fs == FRAME_CLEARED;
}

static int get_frame_stacktop_limit(Frame *frame) {
PyCodeObject *code = get_frame_code(frame);
return code->co_stacksize + code->co_nlocals;
}

static int get_frame_stacktop(Frame *frame) {
assert(frame->f_localsplus);
int stacktop = (int)(frame->f_stacktop - frame->f_localsplus);
assert(stacktop >= 0 && stacktop < get_frame_stacktop_limit(frame));
return stacktop;
}

static void set_frame_stacktop(Frame *frame, int stacktop) {
assert(stacktop >= 0 && stacktop < get_frame_stacktop_limit(frame));
assert(frame->f_localsplus);
frame->f_stacktop = frame->f_localsplus + stacktop;
}

static PyObject **get_frame_localsplus(Frame *frame) {
PyObject **localsplus = frame->f_localsplus;
assert(localsplus);
return localsplus;
}

static int get_frame_iblock_limit(Frame *frame) {
return CO_MAXBLOCKS;
}

static int get_frame_iblock(Frame *frame) {
return frame->f_iblock;
}

static void set_frame_iblock(Frame *frame, int iblock) {
assert(iblock >= 0 && iblock < get_frame_iblock_limit(frame));
frame->f_iblock = iblock;
}

static PyTryBlock *get_frame_blockstack(Frame *frame) {
PyTryBlock *blockstack = frame->f_blockstack;
assert(blockstack);
return blockstack;
}

Loading