Skip to content

Commit fcf0d0d

Browse files
Add the ability to reseed rng in uuid_utils (#86)
1 parent 2c980ca commit fcf0d0d

File tree

5 files changed

+55
-1
lines changed

5 files changed

+55
-1
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ in comparison with the `uuid_utils` default behaviour, but is still faster than
7474
UUID('ffe95fcc-b818-4aca-a350-e0a35b9de6ec')
7575
```
7676

77+
## Interaction with Fork
78+
79+
The underlying rng of this library is not guaranteed to be reset when you fork this process. This would mean that `uuid_utils` calls post fork could result in the same value across processes.
80+
81+
If you plan to use this library alongside forking you will want to explicitly redeed the rng post fork. Either by calling `reseed_rng` manually or registering it in `os.register_at_fork`.
82+
7783
## Benchmarks
7884

7985
| Benchmark | Min | Max | Mean | Min (+) | Max (+) | Mean (+) |

python/uuid_utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
UUID,
1313
__version__,
1414
getnode,
15+
reseed_rng,
1516
uuid1,
1617
uuid3,
1718
uuid4,
@@ -34,6 +35,7 @@
3435
"SafeUUID",
3536
"__version__",
3637
"getnode",
38+
"reseed_rng",
3739
"uuid1",
3840
"uuid3",
3941
"uuid4",

python/uuid_utils/__init__.pyi

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,17 @@ class UUID:
124124
def __ge__(self, other: UUID) -> bool: ...
125125

126126
def getnode() -> int: ...
127+
def reseed_rng() -> None:
128+
"""
129+
Reseeds the underlying rng.
130+
This is useful in cases where you fork, as without reseeding the
131+
generated uuids may be identical. This can be called manually in the child process,
132+
or automatically run after fork with:
133+
134+
os.register_at_fork(after_in_child=uuid_utils.reseed_rng)
135+
"""
136+
...
137+
127138
def uuid1(node: int | None = None, clock_seq: int | None = None) -> UUID:
128139
"""Generate a UUID from a host ID, sequence number, and the current time.
129140
If 'node' is not given, getnode() is used to obtain the hardware

src/lib.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use ahash::AHasher;
22
use mac_address::get_mac_address;
33
use pyo3::{
44
prelude::*, ffi,
5-
exceptions::{PyTypeError, PyValueError},
5+
exceptions::{PyOSError, PyTypeError, PyValueError},
66
pyclass::CompareOp,
77
types::{PyBytes, PyDict},
88
IntoPyObjectExt
@@ -448,6 +448,13 @@ fn getnode() -> PyResult<u64> {
448448
Ok(_getnode())
449449
}
450450

451+
#[pyfunction]
452+
fn reseed_rng() -> PyResult<()> {
453+
rand::rng()
454+
.reseed()
455+
.map_err(|err| PyOSError::new_err(err.to_string()))
456+
}
457+
451458
#[pymodule]
452459
fn _uuid_utils(m: &Bound<'_, PyModule>) -> PyResult<()> {
453460
let safe_uuid_unknown = Python::attach(|py| {
@@ -472,6 +479,7 @@ fn _uuid_utils(m: &Bound<'_, PyModule>) -> PyResult<()> {
472479
m.add_function(wrap_pyfunction!(uuid7, m)?)?;
473480
m.add_function(wrap_pyfunction!(uuid8, m)?)?;
474481
m.add_function(wrap_pyfunction!(getnode, m)?)?;
482+
m.add_function(wrap_pyfunction!(reseed_rng, m)?)?;
475483
m.add("NAMESPACE_DNS", UUID::NAMESPACE_DNS)?;
476484
m.add("NAMESPACE_URL", UUID::NAMESPACE_URL)?;
477485
m.add("NAMESPACE_OID", UUID::NAMESPACE_OID)?;

tests/test_uuid.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import copy
2+
import os
23
import pickle
34
import sys
45
from datetime import datetime
@@ -208,3 +209,29 @@ def test_is_safe() -> None:
208209
@pytest.mark.xfail(sys.platform == "linux", reason="Might fail in Github Actions")
209210
def test_getnode() -> None:
210211
assert uuid_utils.getnode() == getnode()
212+
213+
214+
@pytest.mark.skipif(sys.platform == "win32", reason="Does not run on Windows")
215+
def test_reseed_rng_with_fork() -> None:
216+
read_end, write_end = os.pipe()
217+
# forcibly generate uuid before fork to have rng state
218+
uuid_utils.uuid4()
219+
220+
pid = os.fork()
221+
if pid == 0:
222+
os.close(read_end)
223+
# explicity reseed in the child
224+
uuid_utils.reseed_rng()
225+
next_uuid_child = str(uuid_utils.uuid4())
226+
with os.fdopen(write_end, "w") as write_pipe:
227+
write_pipe.write(next_uuid_child)
228+
os._exit(0)
229+
230+
os.close(write_end)
231+
next_parent_uuid = uuid_utils.uuid4()
232+
os.waitpid(pid, 0)
233+
with os.fdopen(read_end) as read_pipe:
234+
uuid_from_pipe = uuid_utils.UUID(read_pipe.read())
235+
236+
# the uuids should be different because we reseeded
237+
assert next_parent_uuid != uuid_from_pipe

0 commit comments

Comments
 (0)