permid64 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- permid64-0.1.0/PKG-INFO +267 -0
- permid64-0.1.0/README.md +240 -0
- permid64-0.1.0/permid64/__init__.py +17 -0
- permid64-0.1.0/permid64/generator.py +122 -0
- permid64-0.1.0/permid64/layout.py +54 -0
- permid64-0.1.0/permid64/permutation.py +175 -0
- permid64-0.1.0/permid64/source.py +105 -0
- permid64-0.1.0/permid64/types.py +12 -0
- permid64-0.1.0/permid64.egg-info/PKG-INFO +267 -0
- permid64-0.1.0/permid64.egg-info/SOURCES.txt +18 -0
- permid64-0.1.0/permid64.egg-info/dependency_links.txt +1 -0
- permid64-0.1.0/permid64.egg-info/requires.txt +7 -0
- permid64-0.1.0/permid64.egg-info/top_level.txt +1 -0
- permid64-0.1.0/pyproject.toml +46 -0
- permid64-0.1.0/setup.cfg +4 -0
- permid64-0.1.0/tests/test_counter.py +101 -0
- permid64-0.1.0/tests/test_id64_e2e.py +147 -0
- permid64-0.1.0/tests/test_layout.py +52 -0
- permid64-0.1.0/tests/test_permutation.py +86 -0
- permid64-0.1.0/tests/test_properties.py +185 -0
permid64-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: permid64
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Clock-free, persistent, reversible-permutation 64-bit ID generation
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/erickh826/permid64
|
|
7
|
+
Project-URL: Issues, https://github.com/erickh826/permid64/issues
|
|
8
|
+
Keywords: id,uuid,unique-id,feistel,permutation,counter,clock-free
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-xdist; extra == "dev"
|
|
24
|
+
Requires-Dist: hypothesis; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# permid64
|
|
29
|
+
|
|
30
|
+
**Clock-free, persistent, reversible-permutation 64-bit ID generation.**
|
|
31
|
+
|
|
32
|
+
> *Counter in, permutation out.*
|
|
33
|
+
|
|
34
|
+
permid64 generates unique 64-bit integer IDs without relying on wall-clock time. It combines a crash-safe persistent counter with an invertible permutation to produce IDs that look random but carry recoverable metadata.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
# Raw counter (leaks business volume at a glance)
|
|
38
|
+
1001, 1002, 1003 ...
|
|
39
|
+
|
|
40
|
+
# permid64 (shuffled surface, recoverable structure)
|
|
41
|
+
12609531668580943872, 7349201938475629, 3847291038012847 ...
|
|
42
|
+
# decode(12609531668580943872) → instance_id=42, sequence=0
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## What it is
|
|
48
|
+
|
|
49
|
+
- A **clock-free** 64-bit ID generator — no timestamp, no NTP dependency
|
|
50
|
+
- IDs are **unique** because the source is a monotonically increasing counter
|
|
51
|
+
- IDs **look shuffled** because they pass through a reversible permutation
|
|
52
|
+
- The permutation is **invertible** — `decode()` recovers the original metadata
|
|
53
|
+
|
|
54
|
+
## What it is not
|
|
55
|
+
|
|
56
|
+
- **Not a timestamp-based scheme** — there is no time component in the ID
|
|
57
|
+
- **Not a UUID replacement for every scenario** — if you need a globally unique random token with no infrastructure at all, UUID v4 is simpler
|
|
58
|
+
- **Not cryptographic encryption** — the permutation is an obfuscation layer, not authenticated encryption; do not use IDs as secrets or security tokens
|
|
59
|
+
- **Not safe for multiple processes sharing one state file** — `PersistentCounterSource` is single-process only; concurrent writes from multiple processes to the same state file will cause duplicates (see [Limitations](#limitations))
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Design
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
seq = source.next() # monotonic counter (persistent)
|
|
67
|
+
raw = layout.compose(instance_id, seq) # pack 16-bit shard + 48-bit seq
|
|
68
|
+
id64 = permutation.forward(raw) # obfuscate with invertible bijection
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Layout** — default 64-bit split:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
[ instance_id : 16 bits ][ sequence : 48 bits ]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- Up to **65 535** independent shards
|
|
78
|
+
- Up to **281 trillion** IDs per shard
|
|
79
|
+
|
|
80
|
+
**Permutations** — both are bijections over `[0, 2^64)`:
|
|
81
|
+
|
|
82
|
+
| Mode | Formula | Speed | Mixing |
|
|
83
|
+
|---|---|---|---|
|
|
84
|
+
| `multiplicative` | `f(x) = (a·x + b) mod 2^64` | ~500 M/s | Good |
|
|
85
|
+
| `feistel` | 64-bit Feistel network | ~150 M/s | Excellent |
|
|
86
|
+
|
|
87
|
+
**Persistence** — block reservation strategy:
|
|
88
|
+
1. On startup, read high-water mark from state file.
|
|
89
|
+
2. Reserve a block of N sequence numbers, write new high-water mark.
|
|
90
|
+
3. Serve IDs from memory until block exhausted.
|
|
91
|
+
4. If the process crashes, the unused block is lost (gap), but **no duplicate is ever issued**.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Quick start
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from permid64 import Id64
|
|
99
|
+
|
|
100
|
+
# Multiplicative (fastest)
|
|
101
|
+
gen = Id64.multiplicative(
|
|
102
|
+
instance_id=42,
|
|
103
|
+
state_file="permid64.state",
|
|
104
|
+
block_size=4096,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
uid = gen.next_u64() # e.g. 12609531668580943872
|
|
108
|
+
meta = gen.decode(uid)
|
|
109
|
+
# DecodedId(raw=2748779069440, instance_id=42, sequence=0)
|
|
110
|
+
print(meta.instance_id, meta.sequence)
|
|
111
|
+
|
|
112
|
+
# Feistel (better statistical mixing)
|
|
113
|
+
gen2 = Id64.feistel(
|
|
114
|
+
instance_id=42,
|
|
115
|
+
state_file="permid64.state",
|
|
116
|
+
block_size=4096,
|
|
117
|
+
key=0xDEADBEEFCAFEBABE,
|
|
118
|
+
rounds=6,
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Why decode() matters
|
|
123
|
+
|
|
124
|
+
In production, when an anomalous ID appears in a log or alert, you can decode it instantly — no DB lookup needed:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
meta = gen.decode(12609531668580943872)
|
|
128
|
+
print(f"Issued by instance {meta.instance_id}, sequence #{meta.sequence}")
|
|
129
|
+
# Issued by instance 42, sequence #0
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This makes incident tracing dramatically faster: you immediately know which shard issued the ID and its approximate position in the issuance history.
|
|
133
|
+
|
|
134
|
+
### Assigning instance_id
|
|
135
|
+
|
|
136
|
+
Assign each process or deployment unit a distinct `instance_id`. Common patterns:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
import os
|
|
140
|
+
|
|
141
|
+
# From environment variable (works in Docker / K8s)
|
|
142
|
+
instance_id = int(os.environ.get("INSTANCE_ID", "0"))
|
|
143
|
+
|
|
144
|
+
# From K8s StatefulSet pod name (e.g. "worker-3" -> 3)
|
|
145
|
+
import re
|
|
146
|
+
pod_name = os.environ.get("POD_NAME", "worker-0")
|
|
147
|
+
instance_id = int(re.search(r"(\d+)$", pod_name).group(1))
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Each `instance_id` gets its own independent sequence space — no coordination needed between shards.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Installation
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pip install permid64 # once published to PyPI
|
|
158
|
+
# or from source:
|
|
159
|
+
pip install -e ".[dev]"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Running tests
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
pytest
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Five acceptance criteria are checked:
|
|
171
|
+
|
|
172
|
+
1. **Uniqueness** — 1 million IDs, zero duplicates
|
|
173
|
+
2. **Invertibility** — `decode(next_u64())` recovers `instance_id` and `sequence`
|
|
174
|
+
3. **Restart safety** — sequence never resets across process restarts
|
|
175
|
+
4. **Gap tolerance** — crash causes a gap, never a duplicate
|
|
176
|
+
5. **Thread safety** — concurrent generation remains unique
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Benchmark
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
python benchmarks/bench_id64.py
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Sample output (Apple M2):
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
[Permutation comparison — block_size=4096]
|
|
190
|
+
multiplicative (default keys) ~480,000,000 IDs/sec
|
|
191
|
+
feistel (6 rounds) ~140,000,000 IDs/sec
|
|
192
|
+
feistel (12 rounds) ~80,000,000 IDs/sec
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Guarantees
|
|
198
|
+
|
|
199
|
+
| Guarantee | Notes |
|
|
200
|
+
|---|---|
|
|
201
|
+
| No duplicate IDs within a shard | Strict |
|
|
202
|
+
| No duplicates across restarts | Strict — state file must be on durable storage |
|
|
203
|
+
| Decodable | Only with the same permutation key / params |
|
|
204
|
+
| Gaps allowed | After a crash, some sequence numbers are skipped |
|
|
205
|
+
| No global coordination | Each `instance_id` is fully independent |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Limitations
|
|
210
|
+
|
|
211
|
+
### Single-process only
|
|
212
|
+
|
|
213
|
+
`PersistentCounterSource` is **not safe for concurrent use across multiple processes** sharing the same state file. A best-effort `fcntl.flock` advisory lock is applied during block reservation on POSIX systems, but this is not a hard guarantee — do not rely on it as a substitute for proper shard isolation.
|
|
214
|
+
|
|
215
|
+
The correct pattern for multiple processes is to assign each a **distinct `instance_id`** and a **distinct state file**. Multi-process coordination via a central allocator is planned for v0.3.
|
|
216
|
+
|
|
217
|
+
### Feistel is obfuscation, not encryption
|
|
218
|
+
|
|
219
|
+
The Feistel permutation provides strong mixing and is reversible, but it is not a formally audited cryptographic primitive. Do not rely on it for access control, token authentication, or any security-sensitive use case.
|
|
220
|
+
|
|
221
|
+
### instance_id must be assigned manually
|
|
222
|
+
|
|
223
|
+
There is no automatic shard coordination. Assign `instance_id` values via config or environment variables and ensure they are unique across your deployment.
|
|
224
|
+
|
|
225
|
+
### Sequence space is large but finite
|
|
226
|
+
|
|
227
|
+
The default 48-bit sequence space supports ~281 trillion IDs per shard. This is enough for virtually all workloads, but it is not infinite.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Architecture
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
permid64/
|
|
235
|
+
__init__.py # public exports: Id64, DecodedId
|
|
236
|
+
generator.py # Id64 façade
|
|
237
|
+
source.py # PersistentCounterSource
|
|
238
|
+
layout.py # Layout64 — pack/unpack 64-bit raw value
|
|
239
|
+
permutation.py # MultiplyOddPermutation, Feistel64Permutation
|
|
240
|
+
types.py # DecodedId dataclass
|
|
241
|
+
|
|
242
|
+
tests/
|
|
243
|
+
test_counter.py
|
|
244
|
+
test_layout.py
|
|
245
|
+
test_permutation.py
|
|
246
|
+
test_id64_e2e.py # the 5 MVP acceptance tests
|
|
247
|
+
|
|
248
|
+
benchmarks/
|
|
249
|
+
bench_id64.py
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Roadmap
|
|
255
|
+
|
|
256
|
+
| Version | Focus |
|
|
257
|
+
|---|---|
|
|
258
|
+
| v0.1 (current) | Core: counter + permutation + decode |
|
|
259
|
+
| v0.2 | `IdentityPermutation`, Base32/Base62 encoding, `Id64Config` |
|
|
260
|
+
| v0.3 | Multi-process file locking, `ReservedBlockSource` (central allocator) |
|
|
261
|
+
| v0.4+ | Rust/Go reference implementations, formal cross-language spec |
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## License
|
|
266
|
+
|
|
267
|
+
MIT
|
permid64-0.1.0/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# permid64
|
|
2
|
+
|
|
3
|
+
**Clock-free, persistent, reversible-permutation 64-bit ID generation.**
|
|
4
|
+
|
|
5
|
+
> *Counter in, permutation out.*
|
|
6
|
+
|
|
7
|
+
permid64 generates unique 64-bit integer IDs without relying on wall-clock time. It combines a crash-safe persistent counter with an invertible permutation to produce IDs that look random but carry recoverable metadata.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
# Raw counter (leaks business volume at a glance)
|
|
11
|
+
1001, 1002, 1003 ...
|
|
12
|
+
|
|
13
|
+
# permid64 (shuffled surface, recoverable structure)
|
|
14
|
+
12609531668580943872, 7349201938475629, 3847291038012847 ...
|
|
15
|
+
# decode(12609531668580943872) → instance_id=42, sequence=0
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## What it is
|
|
21
|
+
|
|
22
|
+
- A **clock-free** 64-bit ID generator — no timestamp, no NTP dependency
|
|
23
|
+
- IDs are **unique** because the source is a monotonically increasing counter
|
|
24
|
+
- IDs **look shuffled** because they pass through a reversible permutation
|
|
25
|
+
- The permutation is **invertible** — `decode()` recovers the original metadata
|
|
26
|
+
|
|
27
|
+
## What it is not
|
|
28
|
+
|
|
29
|
+
- **Not a timestamp-based scheme** — there is no time component in the ID
|
|
30
|
+
- **Not a UUID replacement for every scenario** — if you need a globally unique random token with no infrastructure at all, UUID v4 is simpler
|
|
31
|
+
- **Not cryptographic encryption** — the permutation is an obfuscation layer, not authenticated encryption; do not use IDs as secrets or security tokens
|
|
32
|
+
- **Not safe for multiple processes sharing one state file** — `PersistentCounterSource` is single-process only; concurrent writes from multiple processes to the same state file will cause duplicates (see [Limitations](#limitations))
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Design
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
seq = source.next() # monotonic counter (persistent)
|
|
40
|
+
raw = layout.compose(instance_id, seq) # pack 16-bit shard + 48-bit seq
|
|
41
|
+
id64 = permutation.forward(raw) # obfuscate with invertible bijection
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Layout** — default 64-bit split:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
[ instance_id : 16 bits ][ sequence : 48 bits ]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- Up to **65 535** independent shards
|
|
51
|
+
- Up to **281 trillion** IDs per shard
|
|
52
|
+
|
|
53
|
+
**Permutations** — both are bijections over `[0, 2^64)`:
|
|
54
|
+
|
|
55
|
+
| Mode | Formula | Speed | Mixing |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `multiplicative` | `f(x) = (a·x + b) mod 2^64` | ~500 M/s | Good |
|
|
58
|
+
| `feistel` | 64-bit Feistel network | ~150 M/s | Excellent |
|
|
59
|
+
|
|
60
|
+
**Persistence** — block reservation strategy:
|
|
61
|
+
1. On startup, read high-water mark from state file.
|
|
62
|
+
2. Reserve a block of N sequence numbers, write new high-water mark.
|
|
63
|
+
3. Serve IDs from memory until block exhausted.
|
|
64
|
+
4. If the process crashes, the unused block is lost (gap), but **no duplicate is ever issued**.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from permid64 import Id64
|
|
72
|
+
|
|
73
|
+
# Multiplicative (fastest)
|
|
74
|
+
gen = Id64.multiplicative(
|
|
75
|
+
instance_id=42,
|
|
76
|
+
state_file="permid64.state",
|
|
77
|
+
block_size=4096,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
uid = gen.next_u64() # e.g. 12609531668580943872
|
|
81
|
+
meta = gen.decode(uid)
|
|
82
|
+
# DecodedId(raw=2748779069440, instance_id=42, sequence=0)
|
|
83
|
+
print(meta.instance_id, meta.sequence)
|
|
84
|
+
|
|
85
|
+
# Feistel (better statistical mixing)
|
|
86
|
+
gen2 = Id64.feistel(
|
|
87
|
+
instance_id=42,
|
|
88
|
+
state_file="permid64.state",
|
|
89
|
+
block_size=4096,
|
|
90
|
+
key=0xDEADBEEFCAFEBABE,
|
|
91
|
+
rounds=6,
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Why decode() matters
|
|
96
|
+
|
|
97
|
+
In production, when an anomalous ID appears in a log or alert, you can decode it instantly — no DB lookup needed:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
meta = gen.decode(12609531668580943872)
|
|
101
|
+
print(f"Issued by instance {meta.instance_id}, sequence #{meta.sequence}")
|
|
102
|
+
# Issued by instance 42, sequence #0
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This makes incident tracing dramatically faster: you immediately know which shard issued the ID and its approximate position in the issuance history.
|
|
106
|
+
|
|
107
|
+
### Assigning instance_id
|
|
108
|
+
|
|
109
|
+
Assign each process or deployment unit a distinct `instance_id`. Common patterns:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import os
|
|
113
|
+
|
|
114
|
+
# From environment variable (works in Docker / K8s)
|
|
115
|
+
instance_id = int(os.environ.get("INSTANCE_ID", "0"))
|
|
116
|
+
|
|
117
|
+
# From K8s StatefulSet pod name (e.g. "worker-3" -> 3)
|
|
118
|
+
import re
|
|
119
|
+
pod_name = os.environ.get("POD_NAME", "worker-0")
|
|
120
|
+
instance_id = int(re.search(r"(\d+)$", pod_name).group(1))
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Each `instance_id` gets its own independent sequence space — no coordination needed between shards.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Installation
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pip install permid64 # once published to PyPI
|
|
131
|
+
# or from source:
|
|
132
|
+
pip install -e ".[dev]"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Running tests
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
pytest
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Five acceptance criteria are checked:
|
|
144
|
+
|
|
145
|
+
1. **Uniqueness** — 1 million IDs, zero duplicates
|
|
146
|
+
2. **Invertibility** — `decode(next_u64())` recovers `instance_id` and `sequence`
|
|
147
|
+
3. **Restart safety** — sequence never resets across process restarts
|
|
148
|
+
4. **Gap tolerance** — crash causes a gap, never a duplicate
|
|
149
|
+
5. **Thread safety** — concurrent generation remains unique
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Benchmark
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
python benchmarks/bench_id64.py
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Sample output (Apple M2):
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
[Permutation comparison — block_size=4096]
|
|
163
|
+
multiplicative (default keys) ~480,000,000 IDs/sec
|
|
164
|
+
feistel (6 rounds) ~140,000,000 IDs/sec
|
|
165
|
+
feistel (12 rounds) ~80,000,000 IDs/sec
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Guarantees
|
|
171
|
+
|
|
172
|
+
| Guarantee | Notes |
|
|
173
|
+
|---|---|
|
|
174
|
+
| No duplicate IDs within a shard | Strict |
|
|
175
|
+
| No duplicates across restarts | Strict — state file must be on durable storage |
|
|
176
|
+
| Decodable | Only with the same permutation key / params |
|
|
177
|
+
| Gaps allowed | After a crash, some sequence numbers are skipped |
|
|
178
|
+
| No global coordination | Each `instance_id` is fully independent |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Limitations
|
|
183
|
+
|
|
184
|
+
### Single-process only
|
|
185
|
+
|
|
186
|
+
`PersistentCounterSource` is **not safe for concurrent use across multiple processes** sharing the same state file. A best-effort `fcntl.flock` advisory lock is applied during block reservation on POSIX systems, but this is not a hard guarantee — do not rely on it as a substitute for proper shard isolation.
|
|
187
|
+
|
|
188
|
+
The correct pattern for multiple processes is to assign each a **distinct `instance_id`** and a **distinct state file**. Multi-process coordination via a central allocator is planned for v0.3.
|
|
189
|
+
|
|
190
|
+
### Feistel is obfuscation, not encryption
|
|
191
|
+
|
|
192
|
+
The Feistel permutation provides strong mixing and is reversible, but it is not a formally audited cryptographic primitive. Do not rely on it for access control, token authentication, or any security-sensitive use case.
|
|
193
|
+
|
|
194
|
+
### instance_id must be assigned manually
|
|
195
|
+
|
|
196
|
+
There is no automatic shard coordination. Assign `instance_id` values via config or environment variables and ensure they are unique across your deployment.
|
|
197
|
+
|
|
198
|
+
### Sequence space is large but finite
|
|
199
|
+
|
|
200
|
+
The default 48-bit sequence space supports ~281 trillion IDs per shard. This is enough for virtually all workloads, but it is not infinite.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Architecture
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
permid64/
|
|
208
|
+
__init__.py # public exports: Id64, DecodedId
|
|
209
|
+
generator.py # Id64 façade
|
|
210
|
+
source.py # PersistentCounterSource
|
|
211
|
+
layout.py # Layout64 — pack/unpack 64-bit raw value
|
|
212
|
+
permutation.py # MultiplyOddPermutation, Feistel64Permutation
|
|
213
|
+
types.py # DecodedId dataclass
|
|
214
|
+
|
|
215
|
+
tests/
|
|
216
|
+
test_counter.py
|
|
217
|
+
test_layout.py
|
|
218
|
+
test_permutation.py
|
|
219
|
+
test_id64_e2e.py # the 5 MVP acceptance tests
|
|
220
|
+
|
|
221
|
+
benchmarks/
|
|
222
|
+
bench_id64.py
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Roadmap
|
|
228
|
+
|
|
229
|
+
| Version | Focus |
|
|
230
|
+
|---|---|
|
|
231
|
+
| v0.1 (current) | Core: counter + permutation + decode |
|
|
232
|
+
| v0.2 | `IdentityPermutation`, Base32/Base62 encoding, `Id64Config` |
|
|
233
|
+
| v0.3 | Multi-process file locking, `ReservedBlockSource` (central allocator) |
|
|
234
|
+
| v0.4+ | Rust/Go reference implementations, formal cross-language spec |
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
permid64 — Clock-free, persistent, obfuscated 64-bit ID generation.
|
|
3
|
+
|
|
4
|
+
Public API
|
|
5
|
+
----------
|
|
6
|
+
from permid64 import Id64, DecodedId
|
|
7
|
+
|
|
8
|
+
gen = Id64.multiplicative(instance_id=1, state_file="id64.state")
|
|
9
|
+
uid = gen.next_u64()
|
|
10
|
+
meta = gen.decode(uid) # DecodedId(raw=..., instance_id=1, sequence=0)
|
|
11
|
+
"""
|
|
12
|
+
from .generator import Id64
|
|
13
|
+
from .permutation import Permutation64Protocol
|
|
14
|
+
from .types import DecodedId
|
|
15
|
+
|
|
16
|
+
__all__ = ["Id64", "DecodedId", "Permutation64Protocol"]
|
|
17
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
generator.py — Id64: the main public façade.
|
|
3
|
+
|
|
4
|
+
Usage
|
|
5
|
+
-----
|
|
6
|
+
# Multiplicative (fast, simpler)
|
|
7
|
+
gen = Id64.multiplicative(
|
|
8
|
+
instance_id=42,
|
|
9
|
+
state_file="id64.state",
|
|
10
|
+
block_size=4096,
|
|
11
|
+
a=0x9E3779B185EBCA87,
|
|
12
|
+
b=0x6A09E667F3BCC909,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Feistel (better statistical mixing)
|
|
16
|
+
gen = Id64.feistel(
|
|
17
|
+
instance_id=42,
|
|
18
|
+
state_file="id64.state",
|
|
19
|
+
block_size=4096,
|
|
20
|
+
key=0xDEADBEEFCAFEBABE,
|
|
21
|
+
rounds=6,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
id_val = gen.next_u64() # -> int (unsigned 64-bit)
|
|
25
|
+
meta = gen.decode(id_val) # -> DecodedId(raw, instance_id, sequence)
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from .layout import Layout64
|
|
30
|
+
from .permutation import Feistel64Permutation, MultiplyOddPermutation, Permutation64Protocol
|
|
31
|
+
from .source import PersistentCounterSource
|
|
32
|
+
from .types import DecodedId
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Id64:
|
|
36
|
+
"""
|
|
37
|
+
Clock-free 64-bit ID generator.
|
|
38
|
+
|
|
39
|
+
Architecture
|
|
40
|
+
------------
|
|
41
|
+
seq = source.next() # monotonic counter (persistent)
|
|
42
|
+
raw = layout.compose(instance_id, seq) # pack bits
|
|
43
|
+
id64 = permutation.forward(raw) # obfuscate
|
|
44
|
+
|
|
45
|
+
Decode
|
|
46
|
+
------
|
|
47
|
+
raw = permutation.inverse(id64)
|
|
48
|
+
meta = layout.decompose(raw)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
instance_id: int,
|
|
54
|
+
source: PersistentCounterSource,
|
|
55
|
+
permutation: Permutation64Protocol,
|
|
56
|
+
layout: Layout64 | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
self.instance_id = instance_id
|
|
59
|
+
self.source = source
|
|
60
|
+
self.permutation = permutation
|
|
61
|
+
self.layout = layout or Layout64()
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
# Factory constructors
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def multiplicative(
|
|
69
|
+
cls,
|
|
70
|
+
instance_id: int,
|
|
71
|
+
state_file: str,
|
|
72
|
+
block_size: int = 4096,
|
|
73
|
+
a: int = 0x9E3779B185EBCA87,
|
|
74
|
+
b: int = 0x6A09E667F3BCC909,
|
|
75
|
+
) -> "Id64":
|
|
76
|
+
"""
|
|
77
|
+
Create a generator backed by a multiply-odd (affine) permutation.
|
|
78
|
+
|
|
79
|
+
``a`` defaults to the 64-bit golden-ratio constant; ``b`` adds a
|
|
80
|
+
second independent mixing constant. Both can be overridden.
|
|
81
|
+
"""
|
|
82
|
+
return cls(
|
|
83
|
+
instance_id=instance_id,
|
|
84
|
+
source=PersistentCounterSource(state_file, block_size),
|
|
85
|
+
permutation=MultiplyOddPermutation(a=a, b=b),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def feistel(
|
|
90
|
+
cls,
|
|
91
|
+
instance_id: int,
|
|
92
|
+
state_file: str,
|
|
93
|
+
block_size: int = 4096,
|
|
94
|
+
key: int = 0xDEADBEEFCAFEBABE,
|
|
95
|
+
rounds: int = 6,
|
|
96
|
+
) -> "Id64":
|
|
97
|
+
"""
|
|
98
|
+
Create a generator backed by a 64-bit Feistel-network permutation.
|
|
99
|
+
|
|
100
|
+
``key`` is a 64-bit seed from which round keys are derived.
|
|
101
|
+
``rounds`` defaults to 6 (good mixing / speed balance).
|
|
102
|
+
"""
|
|
103
|
+
return cls(
|
|
104
|
+
instance_id=instance_id,
|
|
105
|
+
source=PersistentCounterSource(state_file, block_size),
|
|
106
|
+
permutation=Feistel64Permutation(key=key, rounds=rounds),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# Core API
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def next_u64(self) -> int:
|
|
114
|
+
"""Return the next unique, obfuscated 64-bit ID."""
|
|
115
|
+
seq = self.source.next()
|
|
116
|
+
raw = self.layout.compose(self.instance_id, seq)
|
|
117
|
+
return self.permutation.forward(raw)
|
|
118
|
+
|
|
119
|
+
def decode(self, id64: int) -> DecodedId:
|
|
120
|
+
"""Reverse a previously generated ID back to its metadata."""
|
|
121
|
+
raw = self.permutation.inverse(id64)
|
|
122
|
+
return self.layout.decompose(raw)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
layout.py — Pack / unpack instance_id + sequence into a single 64-bit integer.
|
|
3
|
+
|
|
4
|
+
Default split: 16 bits for instance_id (up to 65 535 shards)
|
|
5
|
+
48 bits for sequence (up to 281 trillion IDs per shard)
|
|
6
|
+
"""
|
|
7
|
+
from .types import DecodedId
|
|
8
|
+
|
|
9
|
+
MASK64 = 0xFFFFFFFFFFFFFFFF
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Layout64:
|
|
13
|
+
"""
|
|
14
|
+
Bit-layout for the 64-bit raw value.
|
|
15
|
+
|
|
16
|
+
instance_id occupies the top `instance_bits` bits.
|
|
17
|
+
sequence occupies the bottom `sequence_bits` bits.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, instance_bits: int = 16, sequence_bits: int = 48) -> None:
|
|
21
|
+
if instance_bits + sequence_bits != 64:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f"instance_bits ({instance_bits}) + sequence_bits ({sequence_bits}) must equal 64"
|
|
24
|
+
)
|
|
25
|
+
self.instance_bits = instance_bits
|
|
26
|
+
self.sequence_bits = sequence_bits
|
|
27
|
+
self.sequence_mask = (1 << sequence_bits) - 1
|
|
28
|
+
self.instance_mask = (1 << instance_bits) - 1
|
|
29
|
+
|
|
30
|
+
def compose(self, instance_id: int, sequence: int) -> int:
|
|
31
|
+
"""
|
|
32
|
+
Pack instance_id and sequence into a single 64-bit integer.
|
|
33
|
+
|
|
34
|
+
Both values are silently masked to their configured bit widths.
|
|
35
|
+
Values exceeding the field width (e.g. instance_id >= 2^instance_bits)
|
|
36
|
+
will have their high bits truncated without raising an error.
|
|
37
|
+
Use ``instance_mask`` / ``sequence_mask`` to validate inputs if
|
|
38
|
+
strict overflow detection is required.
|
|
39
|
+
"""
|
|
40
|
+
if sequence > self.sequence_mask:
|
|
41
|
+
raise OverflowError(
|
|
42
|
+
f"sequence {sequence} exceeds {self.sequence_bits}-bit maximum "
|
|
43
|
+
f"({self.sequence_mask}). The ID space for this shard is exhausted."
|
|
44
|
+
)
|
|
45
|
+
return (
|
|
46
|
+
((instance_id & self.instance_mask) << self.sequence_bits)
|
|
47
|
+
| (sequence & self.sequence_mask)
|
|
48
|
+
) & MASK64
|
|
49
|
+
|
|
50
|
+
def decompose(self, raw: int) -> DecodedId:
|
|
51
|
+
"""Unpack a raw 64-bit integer back into instance_id and sequence."""
|
|
52
|
+
seq = raw & self.sequence_mask
|
|
53
|
+
instance_id = (raw >> self.sequence_bits) & self.instance_mask
|
|
54
|
+
return DecodedId(raw=raw, instance_id=instance_id, sequence=seq)
|