Skip to content

Commit e8a1555

Browse files
committed
Implement AsyncSonyFlake
1 parent 87cb7cb commit e8a1555

File tree

11 files changed

+375
-11
lines changed

11 files changed

+375
-11
lines changed

README.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,23 @@ Turbo mode:
4545
t = timeit(lambda: [next(sf) for _ in range(1000)], number=1000)
4646
print(f"Speed: 1M ids / {t:.2f}sec with {count} machine IDs")
4747
48+
Async:
49+
50+
.. code-block:: python
51+
52+
from anyio import sleep # AsyncSonyFlake supports both asyncio and trio
53+
from sonyflake_turbo import AsyncSonyFlake, SonyFlake
54+
55+
sf = SonyFlake(0x1337, 0xCAFE, start_time=1749081600)
56+
asf = AsyncSonyFlake(sf, sleep)
57+
58+
print(await asf)
59+
print(await asf(5))
60+
61+
async for id_ in asf:
62+
print(id_)
63+
break
64+
4865
Important Notes
4966
===============
5067

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ dev = [
3131
"isort",
3232
"mypy",
3333
"pytest",
34+
"pytest-asyncio",
35+
"pytest-trio",
3436
]
3537

3638
[tool.setuptools]
@@ -66,4 +68,4 @@ warn_unused_configs = true
6668

6769
[tool.cibuildwheel]
6870
test-command = "pytest --import-mode=importlib -p no:cacheprovider {project}/tests"
69-
test-requires = ["pytest"]
71+
test-requires = ["pytest", "pytest-asyncio", "pytest-trio"]

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"src/sonyflake_turbo/sonyflake.c",
3535
"src/sonyflake_turbo/machine_ids.c",
3636
"src/sonyflake_turbo/sleep_wrapper.c",
37+
"src/sonyflake_turbo/async.c",
3738
],
3839
define_macros=define_macros,
3940
py_limited_api=py_limited_api,

src/sonyflake_turbo/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
SONYFLAKE_SEQUENCE_BITS,
77
SONYFLAKE_SEQUENCE_MAX,
88
SONYFLAKE_TIME_OFFSET,
9+
AsyncSonyFlake,
910
MachineIDLCG,
1011
SonyFlake,
1112
)
@@ -18,6 +19,7 @@
1819
"SONYFLAKE_SEQUENCE_BITS",
1920
"SONYFLAKE_SEQUENCE_MAX",
2021
"SONYFLAKE_TIME_OFFSET",
22+
"AsyncSonyFlake",
2123
"MachineIDLCG",
2224
"SonyFlake",
2325
]

src/sonyflake_turbo/_sonyflake.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55

66
#include "module.h"
77
#include "sonyflake.h"
8+
#include "async.h"
89
#include "machine_ids.h"
910
#include "sleep_wrapper.h"
1011

1112
static int sonyflake_module_traverse(PyObject *m, visitproc visit, void *arg) {
1213
struct sonyflake_module_state *state = PyModule_GetState(m);
1314
Py_VISIT(state->sonyflake_cls);
15+
Py_VISIT(state->async_sonyflake_cls);
1416
Py_VISIT(state->machine_id_lcg_cls);
1517
Py_VISIT(state->sleep_wrapper_cls);
1618
return 0;
@@ -19,6 +21,7 @@ static int sonyflake_module_traverse(PyObject *m, visitproc visit, void *arg) {
1921
static int sonyflake_module_clear(PyObject *m) {
2022
struct sonyflake_module_state *state = PyModule_GetState(m);
2123
Py_CLEAR(state->sonyflake_cls);
24+
Py_CLEAR(state->async_sonyflake_cls);
2225
Py_CLEAR(state->machine_id_lcg_cls);
2326
Py_CLEAR(state->sleep_wrapper_cls);
2427
return 0;
@@ -62,7 +65,9 @@ static int sonyflake_exec(PyObject *module) {
6265
struct sonyflake_module_state *state = PyModule_GetState(module);
6366

6467
state->sonyflake_cls = NULL;
68+
state->async_sonyflake_cls = NULL;
6569
state->machine_id_lcg_cls = NULL;
70+
state->sleep_wrapper_cls = NULL;
6671

6772
state->sonyflake_cls = PyType_FromModuleAndSpec(module, &sonyflake_type_spec, NULL);
6873

@@ -74,6 +79,16 @@ static int sonyflake_exec(PyObject *module) {
7479
goto err;
7580
}
7681

82+
state->async_sonyflake_cls = PyType_FromModuleAndSpec(module, &async_sonyflake_type_spec, NULL);
83+
84+
if (!state->async_sonyflake_cls) {
85+
goto err;
86+
}
87+
88+
if (PyModule_AddObjectRef(module, "AsyncSonyFlake", state->async_sonyflake_cls) < 0) {
89+
goto err;
90+
}
91+
7792
state->machine_id_lcg_cls = PyType_FromModuleAndSpec(module, &machine_id_lcg_spec, NULL);
7893

7994
if (!state->machine_id_lcg_cls) {
@@ -107,6 +122,7 @@ static int sonyflake_exec(PyObject *module) {
107122
err:
108123
Py_CLEAR(state->sleep_wrapper_cls);
109124
Py_CLEAR(state->machine_id_lcg_cls);
125+
Py_CLEAR(state->async_sonyflake_cls);
110126
Py_CLEAR(state->sonyflake_cls);
111127

112128
return -1;

src/sonyflake_turbo/_sonyflake.pyi

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List, Optional, Callable, Awaitable, TypeAlias, TypeVar
1+
from typing import Awaitable, Callable, Generator, List, Optional, TypeAlias, TypeVar
22

33
try:
44
from typing import Self
@@ -15,7 +15,6 @@ SONYFLAKE_TIME_OFFSET: int
1515
AsyncSleep: TypeAlias = Callable[[float], Awaitable[None]]
1616
T = TypeVar("T")
1717

18-
1918
async def sleep_wrapper(obj: T, sleep: AsyncSleep, to_sleep: float) -> T:
2019
"""C version of:
2120
@@ -56,17 +55,97 @@ class SonyFlake:
5655
time.
5756
5857
Important:
59-
The more ids you request, the more other threads has to wait
60-
upon next :meth:`next` or :meth:`n` call.
58+
The more ids you request, the more other threads have to wait
59+
upon the next id(s).
60+
61+
Args:
62+
n: Number of ids to generate. Must be greater than 0.
63+
64+
Raises:
65+
ValueError: if n <= 0
66+
67+
Returns:
68+
List of ids.
69+
"""
70+
71+
class AsyncSonyFlake:
72+
"""Async wrapper for :class:`SonyFlake`.
73+
74+
Implements Awaitable and AsyncIterator protocols.
75+
76+
Important:
77+
Main state is stored in SonyFlake. This class has minimum logic on its
78+
own, the only difference is that instead of doing thread-blocking sleep,
79+
it is delegated to the provided ``sleep``. Instance of
80+
class:`sleep_wrapper` is returned in ``__call__``, ``__anext__`` and
81+
``__await``.
82+
83+
Usage:
84+
85+
.. code-block:: python
86+
87+
import asyncio
88+
89+
sf = SonyFlake(0x1337, 0xCAFE, start_time=1749081600)
90+
asf = AsyncSonyFlake(sf, asyncio.sleep)
91+
92+
print(await asf)
93+
print(await asf(5))
94+
95+
async for id_ in asf:
96+
print(id_)
97+
break # AsyncSonyFlake is an infinite generator
98+
"""
99+
100+
def __init__(self, sf: SonyFlake, sleep: AsyncSleep) -> None:
101+
"""Initialize AsyncSonyFlake ID generator.
102+
103+
Args:
104+
sf: Instance of the :class:`SonyFlake`.
105+
sleep: Either `asyncio.sleep` or `trio.sleep`.
106+
107+
Raises:
108+
ValueError: Invalid values of ``machine_id`` or ``start_time``.
109+
TypeError: ``machine_id`` or ``start_time`` are not integers.
110+
"""
111+
112+
def __call__(self, n: int) -> Awaitable[list[int]]:
113+
"""Generate multiple SonyFlake IDs at once.
114+
115+
Roughly equivalent to `[await asf for _ in range(n)]`, but more
116+
efficient. This method saves on task/context switches and syscalls for
117+
getting current time.
118+
119+
Important:
120+
The more ids you request, the more other coroutines have to wait
121+
upon the next id(s).
61122
62123
Args:
63124
n: Number of ids to generate. Must be greater than 0.
64125
126+
Raises:
127+
ValueError: if n <= 0
128+
65129
Returns:
66130
List of ids.
67131
"""
68132

133+
def __await__(self) -> Generator[None, None, int]:
134+
"""Produce a SonyFlake ID."""
135+
136+
def __anext__(self) -> Awaitable[int]:
137+
"""Produce a SonyFlake ID."""
138+
139+
def __aiter__(self) -> Self:
140+
"""Returns ``self``."""
141+
69142
class MachineIDLCG:
143+
"""A simple LCG producing ints suitable to be used as ``machine_id``.
144+
145+
Intended to be used in examples, tests, or when concurrency is not an issue.
146+
You generally want only one instance of this class per process.
147+
"""
148+
70149
def __new__(cls, seed: int, /) -> Self:
71150
"""Make a LCG.
72151

src/sonyflake_turbo/async.c

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
#include <Python.h>
2+
3+
#include "async.h"
4+
#include "sleep_wrapper.h"
5+
#include "sonyflake.h"
6+
7+
8+
struct async_sonyflake_state {
9+
PyObject_HEAD
10+
11+
struct sonyflake_state *sf;
12+
PyObject *sleep;
13+
};
14+
15+
static int async_sonyflake_clear(PyObject *py_self) {
16+
struct async_sonyflake_state *self = (struct async_sonyflake_state *) py_self;
17+
Py_CLEAR(self->sf);
18+
Py_CLEAR(self->sleep);
19+
return 0;
20+
}
21+
22+
static void async_sonyflake_dealloc(PyObject *py_self) {
23+
async_sonyflake_clear(py_self);
24+
25+
PyTypeObject *tp = Py_TYPE(py_self);
26+
freefunc tp_free = PyType_GetSlot(tp, Py_tp_free);
27+
28+
assert(tp_free != NULL);
29+
30+
tp_free(py_self);
31+
Py_DECREF(tp);
32+
}
33+
34+
static int async_sonyflake_traverse(PyObject *py_self, visitproc visit, void *arg) {
35+
struct async_sonyflake_state *self = (struct async_sonyflake_state *) py_self;
36+
Py_VISIT(self->sf);
37+
Py_VISIT(self->sleep);
38+
Py_VISIT(Py_TYPE(py_self));
39+
return 0;
40+
}
41+
42+
static PyObject *async_sonyflake_new(PyTypeObject *type, PyObject *Py_UNUSED(args), PyObject *Py_UNUSED(kwargs)) {
43+
allocfunc tp_alloc = PyType_GetSlot(type, Py_tp_alloc);
44+
45+
assert(tp_alloc != NULL);
46+
47+
struct async_sonyflake_state *self = (void *) tp_alloc(type, 0);
48+
49+
if (!self) {
50+
return NULL;
51+
}
52+
53+
self->sf = NULL;
54+
self->sleep = NULL;
55+
56+
return (PyObject *) self;
57+
}
58+
59+
static int async_sonyflake_init(PyObject *py_self, PyObject *args, PyObject *Py_UNUSED(kwargs)) {
60+
struct async_sonyflake_state *self = (struct async_sonyflake_state *) py_self;
61+
62+
if (!PyArg_ParseTuple(args, "OO", &self->sf, &self->sleep)) {
63+
return -1;
64+
}
65+
66+
struct sonyflake_module_state *module = PyType_GetModuleState(Py_TYPE(self));
67+
68+
assert(module != NULL);
69+
70+
if (!PyObject_IsInstance((PyObject *) self->sf, module->sonyflake_cls)) {
71+
PyErr_SetString(PyExc_TypeError, "sf must be instance of SonyFlake");
72+
return -1;
73+
}
74+
75+
if (!PyCallable_Check(self->sleep)) {
76+
PyErr_SetString(PyExc_TypeError, "sleep must be callable");
77+
return -1;
78+
}
79+
80+
Py_INCREF(self->sf);
81+
Py_INCREF(self->sleep);
82+
83+
return 0;
84+
}
85+
86+
static inline PyObject *wrap_into_sleep(
87+
struct async_sonyflake_state *self,
88+
struct sonyflake_next_sleep_info *sleep_info,
89+
PyObject *obj
90+
) {
91+
if (!obj) {
92+
return NULL;
93+
}
94+
95+
struct sonyflake_module_state *module = PyType_GetModuleState(Py_TYPE(self));
96+
97+
assert(module);
98+
assert(sleep_info);
99+
100+
struct timespec diff = sleep_info->future;
101+
102+
sub_diff(&diff, &sleep_info->now);
103+
104+
PyObject *coro = PyObject_CallFunction(
105+
module->sleep_wrapper_cls,
106+
"OOd",
107+
obj,
108+
self->sleep,
109+
timespec_to_double(&diff)
110+
);
111+
112+
Py_DECREF(obj);
113+
114+
return coro;
115+
}
116+
117+
static PyObject *async_sonyflake_call(struct async_sonyflake_state *self, PyObject *args) {
118+
struct sonyflake_next_sleep_info sleep_info;
119+
120+
return wrap_into_sleep(self, &sleep_info, sonyflake_next_py(self->sf, args, &sleep_info));
121+
}
122+
123+
static PyObject *async_sonyflake_await(struct async_sonyflake_state *self) {
124+
struct sonyflake_next_sleep_info sleep_info;
125+
126+
return wrap_into_sleep(self, &sleep_info, sonyflake_next_py(self->sf, NULL, &sleep_info));
127+
}
128+
129+
PyType_Slot async_sonyflake_type_slots[] = {
130+
{Py_tp_alloc, PyType_GenericAlloc},
131+
{Py_tp_dealloc, async_sonyflake_dealloc},
132+
{Py_tp_traverse, async_sonyflake_traverse},
133+
{Py_tp_clear, async_sonyflake_clear},
134+
{Py_tp_new, async_sonyflake_new},
135+
{Py_tp_init, async_sonyflake_init},
136+
{Py_tp_call, async_sonyflake_call},
137+
{Py_am_await, async_sonyflake_await},
138+
{Py_am_anext, async_sonyflake_await},
139+
{Py_am_aiter, PyObject_SelfIter},
140+
{0, 0},
141+
};
142+
143+
PyType_Spec async_sonyflake_type_spec = {
144+
.name = MODULE_NAME ".AsyncSonyFlake",
145+
.basicsize = sizeof(struct async_sonyflake_state),
146+
.flags = Py_TPFLAGS_DEFAULT,
147+
.slots = async_sonyflake_type_slots,
148+
};

src/sonyflake_turbo/async.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#pragma once
2+
3+
#include "sonyflake.h"
4+
5+
struct async_sonyflake_state;
6+
7+
extern PyType_Spec async_sonyflake_type_spec;

src/sonyflake_turbo/module.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
struct sonyflake_module_state {
1010
PyObject *sonyflake_cls;
11+
PyObject *async_sonyflake_cls;
1112
PyObject *machine_id_lcg_cls;
1213
PyObject *sleep_wrapper_cls;
1314
};

0 commit comments

Comments
 (0)