Skip to content

Commit 6fd59b3

Browse files
committed
Add a way to generate ids in batches
1 parent 0f5dc5e commit 6fd59b3

File tree

3 files changed

+120
-11
lines changed

3 files changed

+120
-11
lines changed

src/sonyflake_turbo/_sonyflake.c

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ inline void sub_diff(struct timespec *a, const struct timespec *b) {
5151
}
5252
}
5353

54-
inline useconds_t get_time_to_usleep(const struct timespec *diff) {
54+
inline uint64_t get_time_to_usleep(const struct timespec *diff) {
5555
return diff->tv_sec * 1000000 + diff->tv_nsec / 1000;
5656
}
5757

@@ -72,6 +72,11 @@ bool incr_combined_sequence(struct sonyflake_state *self) {
7272
return self->combined_sequence == 0;
7373
}
7474

75+
static inline void get_relative_current_time(struct sonyflake_state *self, struct timespec *now) {
76+
clock_gettime(CLOCK_REALTIME, now);
77+
sub_diff(now, &self->start_time);
78+
}
79+
7580
void sort_machine_ids(uint16_t *machine_ids, size_t machine_ids_len) {
7681
if (machine_ids_len <= 1) {
7782
return;
@@ -234,15 +239,13 @@ static void sonyflake_dealloc(struct sonyflake_state *self) {
234239

235240
static PyObject *sonyflake_next(struct sonyflake_state *self) {
236241
struct timespec now, future;
237-
useconds_t to_sleep = 0;
242+
uint64_t to_sleep = 0;
238243
sonyflake_time current;
239244
uint64_t sonyflake_id;
240245

241-
clock_gettime(CLOCK_REALTIME, &now);
242-
243246
PyThread_acquire_lock(self->lock, 1);
244247

245-
sub_diff(&now, &self->start_time);
248+
get_relative_current_time(self, &now);
246249

247250
current = to_sonyflake_time(&now);
248251

@@ -271,6 +274,67 @@ static PyObject *sonyflake_next(struct sonyflake_state *self) {
271274
return PyLong_FromUnsignedLongLong(sonyflake_id);
272275
}
273276

277+
static PyObject *sonyflake_next_n(struct sonyflake_state *self, Py_ssize_t n) {
278+
assert(n > 0);
279+
280+
PyObject *out = PyList_New(n);
281+
282+
if (!out) {
283+
return NULL;
284+
}
285+
286+
struct timespec now, future;
287+
uint64_t to_sleep = 0;
288+
sonyflake_time current, diff;
289+
290+
PyThread_acquire_lock(self->lock, 1);
291+
292+
get_relative_current_time(self, &now);
293+
294+
current = to_sonyflake_time(&now);
295+
296+
if (self->elapsed_time < current) {
297+
self->elapsed_time = current;
298+
self->combined_sequence = 0;
299+
} else if (incr_combined_sequence(self)) {
300+
self->elapsed_time++;
301+
}
302+
303+
PyList_SetItem(out, 0, PyLong_FromUnsignedLongLong(compose(self)));
304+
305+
for (Py_ssize_t i = 1; i < n; i++) {
306+
if (incr_combined_sequence(self)) {
307+
self->elapsed_time++;
308+
}
309+
310+
PyList_SetItem(out, i, PyLong_FromUnsignedLongLong(compose(self)));
311+
}
312+
313+
diff = self->elapsed_time - current;
314+
315+
if (diff <= 0) {
316+
PyThread_release_lock(self->lock);
317+
return out;
318+
} else if (diff > 1) {
319+
get_relative_current_time(self, &now);
320+
}
321+
322+
from_sonyflake_time(self->elapsed_time, &future);
323+
sub_diff(&future, &now);
324+
325+
to_sleep = get_time_to_usleep(&future);
326+
327+
PyThread_release_lock(self->lock);
328+
329+
if (to_sleep > 0) {
330+
Py_BEGIN_ALLOW_THREADS;
331+
usleep(to_sleep);
332+
Py_END_ALLOW_THREADS;
333+
}
334+
335+
return out;
336+
}
337+
274338
static PyObject *sonyflake_repr(struct sonyflake_state *self) {
275339
PyObject *s, *args_list = PyList_New(self->machine_ids_len + 1);
276340

@@ -287,15 +351,15 @@ static PyObject *sonyflake_repr(struct sonyflake_state *self) {
287351

288352
PyList_SetItem(args_list, self->machine_ids_len, s);
289353

290-
for (Py_ssize_t i = 0; i < self->machine_ids_len; i++) {
354+
for (size_t i = 0; i < self->machine_ids_len; i++) {
291355
s = PyUnicode_FromFormat("%u", (unsigned) self->machine_ids[i]);
292356

293357
if (!s) {
294358
Py_DECREF(args_list);
295359
return NULL;
296360
}
297361

298-
PyList_SetItem(args_list, i, s);
362+
PyList_SetItem(args_list, (Py_ssize_t) i, s);
299363
}
300364

301365
s = PyUnicode_FromString(", ");
@@ -321,6 +385,21 @@ static PyObject *sonyflake_repr(struct sonyflake_state *self) {
321385
return s;
322386
}
323387

388+
static PyObject *sonyflake_call(struct sonyflake_state *self, PyObject *args) {
389+
Py_ssize_t n;
390+
391+
if (!PyArg_ParseTuple(args, "n", &n)) {
392+
return NULL;
393+
}
394+
395+
if (n <= 0) {
396+
PyErr_SetString(PyExc_ValueError, "n must be positive");
397+
return NULL;
398+
}
399+
400+
return sonyflake_next_n(self, n);
401+
}
402+
324403
PyDoc_STRVAR(sonyflake_doc,
325404
"SonyFlake(*machine_id, start_time=None)\n--\n\n"
326405
"SonyFlake ID generator implementation that combines multiple ID generators into one to improve throughput.\n"
@@ -338,6 +417,7 @@ static PyType_Slot sonyflake_type_slots[] = {
338417
{Py_tp_iternext, sonyflake_next},
339418
{Py_tp_new, sonyflake_new},
340419
{Py_tp_init, sonyflake_init},
420+
{Py_tp_call, sonyflake_call},
341421
{Py_tp_doc, sonyflake_doc},
342422
{Py_tp_repr, sonyflake_repr},
343423
{0, 0},

src/sonyflake_turbo/_sonyflake.pyi

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import List, Optional
22

33
try:
44
from typing import Self
@@ -37,6 +37,24 @@ class SonyFlake:
3737
def __next__(self) -> int:
3838
"""Produce a SonyFlake ID."""
3939

40+
def __call__(self, n: int, /) -> List[int]:
41+
"""Generate multiple SonyFlake IDs at once.
42+
43+
Roughly equivalent to `[next(sf) for _ in range(n)]`, but more
44+
efficient. This method saves on syscalls to sleep and getting current
45+
time.
46+
47+
Important:
48+
The more ids you request, the more other threads has to wait
49+
upon next :meth:`next` or :meth:`n` call.
50+
51+
Args:
52+
n: Number of ids to generate. Must be greater than 0.
53+
54+
Returns:
55+
List of ids.
56+
"""
57+
4058
class MachineIDLCG:
4159
def __new__(cls, seed: int, /) -> Self:
4260
"""Make a LCG.

tests/test_sonyflake_turbo.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,21 @@ def test_non_int_start_time() -> None:
5252
)
5353

5454

55-
def test_sonyflake_iter() -> None:
56-
n = 250000
55+
@mark.parametrize(
56+
["use_iter", "n"],
57+
[
58+
(use_iter, n)
59+
for use_iter in [True, False]
60+
for n in [1, 100, 250000]
61+
],
62+
)
63+
def test_sonyflake(use_iter: bool, n: int) -> None:
5764
sf = SonyFlake(0x0000, 0x7F7F, 0xFFFF, start_time=1749081600)
58-
ids = [next(sf) for _ in range(n)]
65+
66+
if use_iter:
67+
ids = [next(sf) for _ in range(n)]
68+
else:
69+
ids = sf(n)
5970

6071
assert len(ids) == n
6172
assert len(set(ids)) == len(ids)

0 commit comments

Comments
 (0)