mopr 0.0.10__py3-none-any.whl

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.
mopr/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """MOPR Python package entry point."""
2
+
3
+ from ._version import __version__
4
+
5
+ __all__ = ["__version__"]
mopr/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.0.10'
22
+ __version_tuple__ = version_tuple = (0, 0, 10)
23
+
24
+ __commit_id__ = commit_id = 'g10dd4e1d7'
@@ -0,0 +1,4 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright (c) Quant1X <wangfengxy@sina.cn>.
3
+ # Licensed under the MIT License.
4
+ """分布式基础设施包 (Python 探索层, 与 C++/Go/Rust 语义对齐)."""
@@ -0,0 +1,48 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright (c) Quant1X <wangfengxy@sina.cn>.
3
+ # Licensed under the MIT License.
4
+ """分布式 ID (Python 探索层).
5
+
6
+ 与 Go/Rust 实现语义对齐: HLC 混合逻辑时钟 + mmap 双槽 checkpoint 持久化
7
+ + 锁字跨进程互斥 + 有界 MPMC 队列.
8
+ """
9
+
10
+ from .generator import CanceledError, Generator
11
+ from .hlc import HLC
12
+ from .id import EPOCH_MS, PAYLOAD_BITS, PHYSICAL_BITS, ID, check_epoch
13
+ from .option import (
14
+ with_clock,
15
+ with_node_count,
16
+ with_seq_bits,
17
+ with_seq_seed,
18
+ with_state_file,
19
+ with_state_strict,
20
+ with_state_sync_every,
21
+ )
22
+ from .queue import Queue, QueueClosedError, QueueEmptyError, QueueFullError
23
+ from .state_store import FileStateStore, PersistentState, StateFileError
24
+
25
+ __all__ = [
26
+ "CanceledError",
27
+ "EPOCH_MS",
28
+ "FileStateStore",
29
+ "Generator",
30
+ "HLC",
31
+ "ID",
32
+ "PAYLOAD_BITS",
33
+ "PHYSICAL_BITS",
34
+ "PersistentState",
35
+ "Queue",
36
+ "QueueClosedError",
37
+ "QueueEmptyError",
38
+ "QueueFullError",
39
+ "StateFileError",
40
+ "check_epoch",
41
+ "with_clock",
42
+ "with_node_count",
43
+ "with_seq_bits",
44
+ "with_seq_seed",
45
+ "with_state_file",
46
+ "with_state_strict",
47
+ "with_state_sync_every",
48
+ ]
@@ -0,0 +1,74 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright (c) Quant1X <wangfengxy@sina.cn>.
3
+ # Licensed under the MIT License.
4
+ """分布式 ID 生成器 (与 Go/Rust 语义对齐).
5
+
6
+ Next() 组合 HLC 水位: (physical - epoch) << 22 | node_id << seq_bits | seq.
7
+ Serve() 持续生产并入队, 响应 cancel 事件与队列关闭 (对齐 Rust 修复后的语义).
8
+ """
9
+
10
+ import threading
11
+ import time
12
+
13
+ from .id import EPOCH_MS, PAYLOAD_BITS, ID, check_epoch
14
+ from .queue import Queue, QueueClosedError, QueueFullError
15
+
16
+
17
+ class CanceledError(Exception):
18
+ """Serve 被取消 (对应 Go 的 context.Canceled)."""
19
+
20
+
21
+ class Generator:
22
+ """按 node_id + HLC 生成全局唯一 ID."""
23
+
24
+ def __init__(self, node_id: int, hlc):
25
+ if hlc is None:
26
+ raise ValueError("hlc 不能为 None")
27
+ self.hlc = hlc
28
+ self.seq_bits = hlc.seq_bits
29
+ self.worker_bits = PAYLOAD_BITS - self.seq_bits
30
+ if node_id < 0 or node_id >= (1 << self.worker_bits):
31
+ raise ValueError(
32
+ f"node_id {node_id} 超出 {self.worker_bits} 位 worker 范围 "
33
+ f"({1 << self.worker_bits} 个节点)"
34
+ )
35
+ self.node_id = node_id
36
+
37
+ def next(self) -> ID:
38
+ """生成下一个 ID (对齐 Go 的 Next)."""
39
+ physical, sequence = self.hlc.now()
40
+ elapsed = check_epoch(physical - EPOCH_MS)
41
+ return ID(
42
+ (elapsed << PAYLOAD_BITS)
43
+ | (self.node_id << self.seq_bits)
44
+ | (sequence & ((1 << self.seq_bits) - 1))
45
+ )
46
+
47
+ def serve(self, queue: Queue, cancel: threading.Event = None) -> None:
48
+ """持续生产并入队, 直到取消或队列关闭 (对齐 Go 的 Serve).
49
+
50
+ cancel 为 threading.Event, set 后返回并抛 CanceledError.
51
+ 队列关闭时正常返回 None; 队列满时重试同一 ID, 每轮检查取消/关闭,
52
+ 避免阻塞在满队列上无法响应取消.
53
+ """
54
+ if queue is None:
55
+ raise ValueError("queue 不能为 None")
56
+ while True:
57
+ if cancel is not None and cancel.is_set():
58
+ raise CanceledError()
59
+ if queue.is_closed():
60
+ return
61
+ identifier = self.next()
62
+ # 队列满时重试同一个 ID, 每轮回到循环顶部检查取消/关闭
63
+ while True:
64
+ if cancel is not None and cancel.is_set():
65
+ raise CanceledError()
66
+ if queue.is_closed():
67
+ return
68
+ try:
69
+ queue.try_push(identifier)
70
+ break
71
+ except QueueFullError:
72
+ time.sleep(0) # 让出 GIL, 对齐 Rust 的 yield_now
73
+ except QueueClosedError:
74
+ return
@@ -0,0 +1,109 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright (c) Quant1X <wangfengxy@sina.cn>.
3
+ # Licensed under the MIT License.
4
+ """混合逻辑时钟 HLC (与 Go 的 hlc.go / Rust 的 hlc.rs 语义对齐).
5
+
6
+ 约定:
7
+ now() 推进并返回 (physical_ms, seq)
8
+ timestamp() 返回 physical_ms (不推进)
9
+ close() 刷新状态文件 (幂等)
10
+
11
+ 物理时钟回拨时通过递增 seq 保证单调; seq 溢出时物理时钟 +1 并重置 seq.
12
+ """
13
+
14
+ import os
15
+ import struct
16
+ import threading
17
+ import time
18
+
19
+ from .option import default_sync_every_value
20
+ from .state_store import FileStateStore, PersistentState
21
+
22
+
23
+ def random_uint16() -> int:
24
+ """进程级随机种子 (对齐 Go 的 randUint16: crypto/rand, 失败时退化)."""
25
+ try:
26
+ return struct.unpack(">H", os.urandom(2))[0]
27
+ except NotImplementedError:
28
+ return (int(time.time_ns()) ^ (os.getpid() << 16)) & 0xFFFF
29
+
30
+
31
+ class HLC:
32
+ """混合逻辑时钟: 41 位物理毫秒 + 22 位 payload (worker + seq)."""
33
+
34
+ def __init__(self, *opts):
35
+ self._lock = threading.Lock()
36
+ self._clock = lambda: int(time.time() * 1000)
37
+ self.physical = 0
38
+ self.seq = 0
39
+ self.seed = random_uint16()
40
+ self.seq_bits = 11 # payloadBits(22) - 默认 2^11 节点
41
+ self.sync_every = default_sync_every_value()
42
+ self.strict = False
43
+ self.state_file = None
44
+ self.store = None
45
+
46
+ for opt in opts:
47
+ opt(self)
48
+
49
+ if self.state_file is not None:
50
+ self.store = FileStateStore(self.state_file, self.sync_every, self.strict)
51
+
52
+ restored = None
53
+ ok = False
54
+ if self.store is not None:
55
+ restored, ok = self.store.load()
56
+ if ok:
57
+ self.physical = restored.physical
58
+ self.seq = restored.seq
59
+ else:
60
+ self.physical = self._clock()
61
+ self.seq = self.seed & self.seq_mask
62
+
63
+ @property
64
+ def seq_mask(self) -> int:
65
+ return (1 << self.seq_bits) - 1
66
+
67
+ def now(self):
68
+ """推进并返回 (physical, seq), 对齐 Go 的 Now()."""
69
+ with self._lock:
70
+ now = self._clock()
71
+ if self.store is not None:
72
+ next_state = self.store.next(
73
+ PersistentState(self.physical, self.seq), now, self.seq_bits
74
+ )
75
+ self.physical = next_state.physical
76
+ self.seq = next_state.seq
77
+ return self.physical, self.seq
78
+
79
+ if now > self.physical:
80
+ self.physical = now
81
+ self.seq = 0
82
+ elif self.seq >= self.seq_mask:
83
+ self.physical += 1
84
+ self.seq = 0
85
+ else:
86
+ self.seq += 1
87
+ return self.physical, self.seq
88
+
89
+ def timestamp(self) -> int:
90
+ """返回物理毫秒, 对齐 Go 的 Timestamp()."""
91
+ return self.physical
92
+
93
+ def close(self) -> None:
94
+ """刷新状态文件并释放映射 (幂等, 对齐 Go 的 Close())."""
95
+ with self._lock:
96
+ if self.store is None:
97
+ return
98
+ first_err = None
99
+ try:
100
+ self.store.flush()
101
+ except Exception as e:
102
+ first_err = e
103
+ try:
104
+ self.store.close()
105
+ except Exception as e:
106
+ if first_err is None:
107
+ first_err = e
108
+ if first_err is not None:
109
+ raise first_err
@@ -0,0 +1,83 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright (c) Quant1X <wangfengxy@sina.cn>.
3
+ # Licensed under the MIT License.
4
+ """分布式 ID 类型与位布局常量 (与 Go/Rust 语义对齐).
5
+
6
+ 位布局 (64 位, 大端):
7
+ | 41 位 physical(ms) | 22 位 payload |
8
+ payload 又分: | worker_bits 位 node_id | seq_bits 位 seq |
9
+ """
10
+
11
+ import base64
12
+
13
+ # 纪元起点: 2026-01-01T00:00:00Z (毫秒), 与 Go/Rust 的 EpochMs 一致
14
+ EPOCH_MS = 1767225600000
15
+ # 22 位 payload: 2^22 = 4194304, 单进程/线程每毫秒容量
16
+ PAYLOAD_BITS = 22
17
+ # 41 位 physical: 2^41 ms ≈ 69.7 年 (自纪元起)
18
+ PHYSICAL_BITS = 41
19
+ # worker 位数上限 (seq 至少保留 4 位)
20
+ MAX_WORKER_BITS = PAYLOAD_BITS - 4
21
+
22
+
23
+ class ID(int):
24
+ """64 位分布式 ID, 语义对齐 Go 的 ID (uint64 包装类型).
25
+
26
+ Python int 无符号溢出限制, 但本布局最大 63 位 (physical 41 + payload 22),
27
+ 与 Go 的 uint64 表示一一对应.
28
+ """
29
+
30
+ def __new__(cls, value: int) -> "ID":
31
+ if value < 0 or value >= (1 << 64):
32
+ raise ValueError(f"ID 超出 uint64 范围: {value}")
33
+ return super().__new__(cls, value)
34
+
35
+ @classmethod
36
+ def from_int(cls, value: int) -> "ID":
37
+ return cls(value)
38
+
39
+ def to_int(self) -> int:
40
+ return int(self)
41
+
42
+ def physical(self) -> int:
43
+ """自纪元起的毫秒数 (高 41 位)."""
44
+ return int(self) >> PAYLOAD_BITS
45
+
46
+ def node_id(self, worker_bits: int) -> int:
47
+ """节点号 (payload 高 worker_bits 位)."""
48
+ if worker_bits < 0 or worker_bits > PAYLOAD_BITS:
49
+ raise ValueError(f"worker_bits 超出范围: {worker_bits}")
50
+ return (int(self) >> (PAYLOAD_BITS - worker_bits)) & ((1 << worker_bits) - 1)
51
+
52
+ def seq(self, worker_bits: int) -> int:
53
+ """序列号 (payload 低 seq_bits 位)."""
54
+ if worker_bits < 0 or worker_bits > PAYLOAD_BITS:
55
+ raise ValueError(f"worker_bits 超出范围: {worker_bits}")
56
+ return int(self) & ((1 << (PAYLOAD_BITS - worker_bits)) - 1)
57
+
58
+ def to_bytes(self) -> bytes:
59
+ """大端 8 字节编码, 对齐 Go 的 Bytes()."""
60
+ return int(self).to_bytes(8, byteorder="big")
61
+
62
+ def string(self) -> str:
63
+ """URL-safe Base64 无填充 11 字符表示, 对齐 Go 的 String().
64
+
65
+ RawURLEncoding: 无 padding, 字符集 [-_0-9A-Za-z].
66
+ """
67
+ return base64.urlsafe_b64encode(self.to_bytes()).rstrip(b"=").decode("ascii")
68
+
69
+ @classmethod
70
+ def from_bytes(cls, data: bytes) -> "ID":
71
+ """解码大端 8 字节, 对齐 Go 的 FromBytes."""
72
+ if len(data) != 8:
73
+ raise ValueError(f"ID 字节长度必须为 8, 实际 {len(data)}")
74
+ return cls(int.from_bytes(data, byteorder="big"))
75
+
76
+
77
+ def check_epoch(elapsed: int) -> int:
78
+ """校验自纪元起的毫秒数落在 physical 41 位可表示范围内, 对齐 Go 的 checkEpoch."""
79
+ if elapsed < 0 or elapsed >= (1 << PHYSICAL_BITS):
80
+ raise ValueError(
81
+ f"自纪元起毫秒数超出 {PHYSICAL_BITS} 位可表示范围: {elapsed}"
82
+ )
83
+ return elapsed
@@ -0,0 +1,80 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright (c) Quant1X <wangfengxy@sina.cn>.
3
+ # Licensed under the MIT License.
4
+ """HLC 构造选项 (与 Go 的 option.go / Rust 的 option 语义对齐).
5
+
6
+ 每个 with_* 返回一个可调用对象, 在 HLC 构造时按其语义应用.
7
+ """
8
+
9
+ import os
10
+
11
+ from .id import PAYLOAD_BITS
12
+
13
+ # 默认每 N 次 Now 落盘一次 checkpoint
14
+ DEFAULT_SYNC_EVERY = 1000
15
+ # seq 保留位数下限 (worker 位上限 18)
16
+ MIN_SEQ_BITS = 4
17
+
18
+
19
+ def default_sync_every_value() -> int:
20
+ """读取环境变量 QUANT1X_ID64_SYNC_EVERY, 非法/缺失时回退默认值, 对齐 Go."""
21
+ raw = os.environ.get("QUANT1X_ID64_SYNC_EVERY")
22
+ if raw is not None:
23
+ try:
24
+ value = int(raw)
25
+ if value > 0:
26
+ return value
27
+ except ValueError:
28
+ pass
29
+ return DEFAULT_SYNC_EVERY
30
+
31
+
32
+ def with_clock(now):
33
+ """自定义毫秒时钟函数 (默认 time.time() * 1000), 对齐 Go 的 WithClock."""
34
+ return lambda hlc: setattr(hlc, "_clock", now)
35
+
36
+
37
+ def with_seq_seed(seed: int) -> object:
38
+ """初始化 seq 的种子值, 对齐 Go 的 WithSeqSeed."""
39
+ if seed < 0 or seed > 0xFFFF:
40
+ raise ValueError(f"seq seed 必须为 uint16, 实际 {seed}")
41
+ return lambda hlc: setattr(hlc, "seed", seed)
42
+
43
+
44
+ def with_state_file(path) -> object:
45
+ """启用跨进程持久化状态文件, 对齐 Go 的 WithStateFile."""
46
+ if not path:
47
+ raise ValueError("state 文件路径不能为空")
48
+ return lambda hlc: setattr(hlc, "state_file", path)
49
+
50
+
51
+ def with_state_sync_every(every: int) -> object:
52
+ """设置 checkpoint 落盘间隔, 对齐 Go 的 WithStateSyncEvery."""
53
+ if every <= 0:
54
+ raise ValueError(f"sync_every 必须为正数, 实际 {every}")
55
+ return lambda hlc: setattr(hlc, "sync_every", every)
56
+
57
+
58
+ def with_state_strict() -> object:
59
+ """严格模式: 每次 Now 先抢跨进程锁再 checkpooint, 对齐 Go 的 WithStateStrict."""
60
+ return lambda hlc: setattr(hlc, "strict", True)
61
+
62
+
63
+ def with_node_count(count: int) -> object:
64
+ """按节点数推导 seq 位数, 对齐 Go 的 WithNodeCount.
65
+
66
+ seq_bits = 22 - ceil(log2(count)); count < 2^18 时 seq_bits >= 4.
67
+ """
68
+ if count < 1:
69
+ count = 1
70
+ seq_bits = PAYLOAD_BITS - count.bit_length()
71
+ if seq_bits < MIN_SEQ_BITS:
72
+ raise ValueError(f"节点数 {count} 过大: 需至少保留 {MIN_SEQ_BITS} 位序列号")
73
+ return lambda hlc: setattr(hlc, "seq_bits", seq_bits)
74
+
75
+
76
+ def with_seq_bits(seq_bits: int) -> object:
77
+ """直接指定 seq 位数 (4..21), 对齐 Go 的 WithSeqBits."""
78
+ if seq_bits < MIN_SEQ_BITS or seq_bits > PAYLOAD_BITS - 1:
79
+ raise ValueError(f"seq_bits 必须在 {MIN_SEQ_BITS}..{PAYLOAD_BITS - 1}, 实际 {seq_bits}")
80
+ return lambda hlc: setattr(hlc, "seq_bits", seq_bits)
@@ -0,0 +1,125 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright (c) Quant1X <wangfengxy@sina.cn>.
3
+ # Licensed under the MIT License.
4
+ """有界多生产者多消费者队列 (对齐 Go runtime.RingBuffer 的语义).
5
+
6
+ API 语义:
7
+ try_push 非阻塞, 满 -> QueueFullError, 已关闭 -> QueueClosedError
8
+ try_pop 非阻塞, 空 -> QueueEmptyError, 已关闭且空 -> QueueClosedError
9
+ push 阻塞直到有空位, 关闭后 -> QueueClosedError
10
+ pop 阻塞直到有数据, 已关闭且空 -> QueueClosedError
11
+ wait_for_close 等待所有已入队数据被消费完毕
12
+
13
+ 容量向上取整为 2 的幂 (与 Go/Rust 一致), 幂等 close.
14
+ Python 探索层使用标准库线程原语实现同等语义 (无锁环缓冲为生产层实现).
15
+ """
16
+
17
+ import collections
18
+ import threading
19
+
20
+
21
+ class QueueError(Exception):
22
+ """队列错误基类."""
23
+
24
+
25
+ class QueueFullError(QueueError):
26
+ """队列已满 (对应 Go 的 ErrQueueFull)."""
27
+
28
+
29
+ class QueueEmptyError(QueueError):
30
+ """队列已空 (对应 Go 的 ErrQueueEmpty)."""
31
+
32
+
33
+ class QueueClosedError(QueueError):
34
+ """队列已关闭 (对应 Go 的 ErrClosed)."""
35
+
36
+
37
+ class Queue:
38
+ """有界 MPMC 队列, 容量向上取整为 2 的幂."""
39
+
40
+ def __init__(self, capacity: int):
41
+ if capacity < 1:
42
+ raise ValueError(f"capacity 必须为正数, 实际 {capacity}")
43
+ cap = 1
44
+ while cap < capacity:
45
+ cap <<= 1
46
+ self._cap = cap
47
+ self._deque = collections.deque()
48
+ self._lock = threading.Lock()
49
+ self._not_full = threading.Condition(self._lock)
50
+ self._not_empty = threading.Condition(self._lock)
51
+ self._closed = False
52
+
53
+ def try_push(self, value) -> None:
54
+ """非阻塞入队, 满 -> QueueFullError, 已关闭 -> QueueClosedError."""
55
+ with self._lock:
56
+ if self._closed:
57
+ raise QueueClosedError()
58
+ if len(self._deque) >= self._cap:
59
+ raise QueueFullError()
60
+ self._deque.append(value)
61
+ self._not_empty.notify()
62
+
63
+ def try_pop(self):
64
+ """非阻塞出队, 空 -> QueueEmptyError, 已关闭且空 -> QueueClosedError."""
65
+ with self._lock:
66
+ if self._deque:
67
+ value = self._deque.popleft()
68
+ self._not_full.notify()
69
+ return value
70
+ if self._closed:
71
+ raise QueueClosedError()
72
+ raise QueueEmptyError()
73
+
74
+ def push(self, value) -> None:
75
+ """阻塞入队直到有空位; 队列关闭后 -> QueueClosedError."""
76
+ with self._not_full:
77
+ while len(self._deque) >= self._cap:
78
+ self._not_full.wait()
79
+ if self._closed:
80
+ raise QueueClosedError()
81
+ self._deque.append(value)
82
+ self._not_empty.notify()
83
+
84
+ def pop(self):
85
+ """阻塞出队直到有数据; 已关闭且空 -> QueueClosedError."""
86
+ with self._not_empty:
87
+ while not self._deque and not self._closed:
88
+ self._not_empty.wait()
89
+ if self._deque:
90
+ value = self._deque.popleft()
91
+ self._not_full.notify()
92
+ return value
93
+ raise QueueClosedError()
94
+
95
+ def len(self) -> int:
96
+ with self._lock:
97
+ return len(self._deque)
98
+
99
+ def cap(self) -> int:
100
+ return self._cap
101
+
102
+ def is_empty(self) -> bool:
103
+ return self.len() == 0
104
+
105
+ def is_full(self) -> bool:
106
+ return self.len() == self._cap
107
+
108
+ def close(self) -> None:
109
+ """关闭队列: 唤醒所有等待者, 幂等 (对齐 Go 的 Close)."""
110
+ with self._lock:
111
+ if self._closed:
112
+ return
113
+ self._closed = True
114
+ self._not_full.notify_all()
115
+ self._not_empty.notify_all()
116
+
117
+ def is_closed(self) -> bool:
118
+ with self._lock:
119
+ return self._closed
120
+
121
+ def wait_for_close(self) -> None:
122
+ """阻塞直到所有已入队数据被消费 (对齐 Go 的 WaitForClose)."""
123
+ with self._not_empty:
124
+ while self._deque:
125
+ self._not_empty.wait()