rocksdb-dedup 0.1.0__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.
@@ -0,0 +1,22 @@
1
+ """
2
+ RocksDB 去重服务 Python 客户端库
3
+
4
+ 基于 RocksDB + 布隆过滤器的高并发持久化去重引擎的 Python 客户端。
5
+
6
+ 快速开始:
7
+ from dedup_client import DedupClient
8
+
9
+ with DedupClient("localhost:50051") as client:
10
+ result = client.check_and_set(
11
+ namespace="spider_news",
12
+ key=b"https://example.com/page/1",
13
+ expire_at_ms=1735689600000,
14
+ )
15
+ print(result) # "inserted" / "duplicate" / "expired_and_replaced"
16
+ """
17
+
18
+ from .client import DedupClient
19
+ from .sharded import ShardedDedupClient, ConsistentHashRing
20
+
21
+ __all__ = ["DedupClient", "ShardedDedupClient", "ConsistentHashRing"]
22
+ __version__ = "0.2.0"
dedup_client/client.py ADDED
@@ -0,0 +1,307 @@
1
+ """
2
+ RocksDB 去重服务 gRPC 客户端
3
+
4
+ 使用示例:
5
+ from dedup_client import DedupClient
6
+
7
+ client = DedupClient("localhost:50051")
8
+
9
+ # 检查并写入
10
+ result = client.check_and_set(
11
+ namespace="spider_news",
12
+ key=b"https://example.com/page/1",
13
+ expire_at_ms=1735689600000,
14
+ metadata=b"some metadata",
15
+ )
16
+ print(result) # "inserted" / "duplicate" / "expired_and_replaced"
17
+
18
+ # 仅检查是否存在
19
+ exists = client.exists("spider_news", b"https://example.com/page/1")
20
+ """
21
+
22
+ import time
23
+ from typing import Optional, List, Tuple
24
+
25
+ import grpc
26
+
27
+ from . import dedup_pb2
28
+ from . import dedup_pb2_grpc
29
+
30
+
31
+ class DedupClient:
32
+ """去重服务 gRPC 客户端
33
+
34
+ 提供基于 RocksDB + 布隆过滤器的高性能持久化去重能力,
35
+ 支持多租户 namespace 隔离、原子检查写入、批量操作等。
36
+
37
+ Attributes:
38
+ addr: 服务端地址,如 "localhost:50051"
39
+ timeout: 默认超时时间(秒)
40
+ """
41
+
42
+ def __init__(self, addr: str, timeout: float = 5.0):
43
+ """
44
+ Args:
45
+ addr: 服务地址,如 "localhost:50051"
46
+ timeout: 默认超时时间(秒)
47
+ """
48
+ self.addr = addr
49
+ self.timeout = timeout
50
+ self.channel = grpc.insecure_channel(addr)
51
+ self.stub = dedup_pb2_grpc.DedupServiceStub(self.channel)
52
+
53
+ def close(self):
54
+ """关闭连接"""
55
+ self.channel.close()
56
+
57
+ def __enter__(self):
58
+ return self
59
+
60
+ def __exit__(self, *args):
61
+ self.close()
62
+
63
+ # -----------------------------------------------------------------------
64
+ # 核心 API
65
+ # -----------------------------------------------------------------------
66
+
67
+ def check_and_set(
68
+ self,
69
+ namespace: str,
70
+ key: bytes,
71
+ expire_at_ms: int,
72
+ metadata: Optional[bytes] = None,
73
+ ) -> str:
74
+ """
75
+ 检查 key 是否存在,不存在则写入(原子操作)
76
+
77
+ Args:
78
+ namespace: 业务命名空间,不同业务之间数据隔离
79
+ key: 去重键(bytes,支持任意二进制 key)
80
+ expire_at_ms: 绝对过期时间(毫秒时间戳)
81
+ metadata: 可选附加元数据
82
+
83
+ Returns:
84
+ "inserted" - 新写入
85
+ "duplicate" - 已存在(未过期)
86
+ "expired_and_replaced" - 已过期,已替换
87
+ """
88
+ req = dedup_pb2.CheckAndSetRequest(
89
+ namespace=namespace,
90
+ key=key,
91
+ expire_at_ms=expire_at_ms,
92
+ metadata=metadata or b"",
93
+ )
94
+ resp = self.stub.CheckAndSet(req, timeout=self.timeout)
95
+ return self._result_to_str(resp.result)
96
+
97
+ def batch_check_and_set(
98
+ self,
99
+ items: List[Tuple[str, bytes, int, Optional[bytes]]],
100
+ ) -> List[str]:
101
+ """
102
+ 批量检查并写入(减少 RPC 次数,高 QPS 场景推荐)
103
+
104
+ Args:
105
+ items: [(namespace, key, expire_at_ms, metadata), ...]
106
+
107
+ Returns:
108
+ 结果列表,顺序与输入一致
109
+ """
110
+ req_items = []
111
+ for ns, key, expire_at, meta in items:
112
+ req_items.append(dedup_pb2.CheckAndSetRequest(
113
+ namespace=ns,
114
+ key=key,
115
+ expire_at_ms=expire_at,
116
+ metadata=meta or b"",
117
+ ))
118
+ req = dedup_pb2.BatchCheckAndSetRequest(items=req_items)
119
+ resp = self.stub.BatchCheckAndSet(req, timeout=self.timeout)
120
+ return [self._result_to_str(r.result) for r in resp.results]
121
+
122
+ def exists(self, namespace: str, key: bytes) -> bool:
123
+ """仅检查 key 是否存在且未过期
124
+
125
+ Args:
126
+ namespace: 业务命名空间
127
+ key: 去重键
128
+
129
+ Returns:
130
+ True 表示存在且未过期,False 表示不存在或已过期
131
+ """
132
+ req = dedup_pb2.ExistsRequest(namespace=namespace, key=key)
133
+ resp = self.stub.Exists(req, timeout=self.timeout)
134
+ return resp.exists
135
+
136
+ def batch_exists(self, namespace: str, keys: List[bytes]) -> List[bool]:
137
+ """批量检查是否存在
138
+
139
+ Args:
140
+ namespace: 业务命名空间(同一 namespace 下的多个 key)
141
+ keys: 去重键列表
142
+
143
+ Returns:
144
+ 存在性列表,顺序与输入一致
145
+ """
146
+ req = dedup_pb2.BatchExistsRequest(namespace=namespace, keys=keys)
147
+ resp = self.stub.BatchExists(req, timeout=self.timeout)
148
+ return list(resp.exists)
149
+
150
+ def get(self, namespace: str, key: bytes) -> Optional[dict]:
151
+ """
152
+ 获取 key 的详细信息
153
+
154
+ Args:
155
+ namespace: 业务命名空间
156
+ key: 去重键
157
+
158
+ Returns:
159
+ {
160
+ "expire_at_ms": int,
161
+ "created_at_ms": int,
162
+ "metadata": bytes,
163
+ }
164
+ 不存在返回 None
165
+ """
166
+ req = dedup_pb2.GetRequest(namespace=namespace, key=key)
167
+ resp = self.stub.Get(req, timeout=self.timeout)
168
+ if not resp.found:
169
+ return None
170
+ return {
171
+ "expire_at_ms": resp.value.expire_at_ms,
172
+ "created_at_ms": resp.value.created_at_ms,
173
+ "metadata": resp.value.metadata,
174
+ }
175
+
176
+ def delete(self, namespace: str, key: bytes) -> bool:
177
+ """删除 key
178
+
179
+ Args:
180
+ namespace: 业务命名空间
181
+ key: 去重键
182
+
183
+ Returns:
184
+ True 表示成功删除,False 表示 key 不存在
185
+ """
186
+ req = dedup_pb2.DeleteRequest(namespace=namespace, key=key)
187
+ resp = self.stub.Delete(req, timeout=self.timeout)
188
+ return resp.deleted
189
+
190
+ def batch_delete(self, namespace: str, keys: List[bytes]) -> List[bool]:
191
+ """批量删除
192
+
193
+ Args:
194
+ namespace: 业务命名空间
195
+ keys: 去重键列表
196
+
197
+ Returns:
198
+ 删除结果列表,顺序与输入一致
199
+ """
200
+ req = dedup_pb2.BatchDeleteRequest(namespace=namespace, keys=keys)
201
+ resp = self.stub.BatchDelete(req, timeout=self.timeout)
202
+ return list(resp.deleted)
203
+
204
+ def stats(self, namespace: str = "") -> dict:
205
+ """
206
+ 获取服务统计信息
207
+
208
+ Args:
209
+ namespace: 可选,指定 namespace;留空返回全局统计
210
+
211
+ Returns:
212
+ {
213
+ "total_keys": int,
214
+ "bloom_capacity": int,
215
+ "bloom_insertions": int,
216
+ "bloom_fpp": float,
217
+ "cleaned_total": int,
218
+ "backlog_estimate": int,
219
+ "current_speed": int,
220
+ "qps_estimate": int,
221
+ "error_count": int,
222
+ }
223
+ """
224
+ req = dedup_pb2.StatsRequest(namespace=namespace)
225
+ resp = self.stub.Stats(req, timeout=self.timeout)
226
+ return {
227
+ "total_keys": resp.total_keys,
228
+ "bloom_capacity": resp.bloom_capacity,
229
+ "bloom_insertions": resp.bloom_insertions,
230
+ "bloom_fpp": resp.bloom_fpp,
231
+ "cleaned_total": resp.cleaned_total,
232
+ "backlog_estimate": resp.backlog_estimate,
233
+ "current_speed": resp.current_speed,
234
+ "qps_estimate": resp.qps_estimate,
235
+ "error_count": resp.error_count,
236
+ }
237
+
238
+ # -----------------------------------------------------------------------
239
+ # 工具方法
240
+ # -----------------------------------------------------------------------
241
+
242
+ @staticmethod
243
+ def _result_to_str(result: int) -> str:
244
+ """将 proto enum 转换为字符串"""
245
+ if result == dedup_pb2.CheckResult.INSERTED:
246
+ return "inserted"
247
+ elif result == dedup_pb2.CheckResult.DUPLICATE:
248
+ return "duplicate"
249
+ elif result == dedup_pb2.CheckResult.EXPIRED_AND_REPLACED:
250
+ return "expired_and_replaced"
251
+ else:
252
+ return f"unknown({result})"
253
+
254
+
255
+ # ---------------------------------------------------------------------------
256
+ # 使用示例
257
+ # ---------------------------------------------------------------------------
258
+
259
+ def main():
260
+ """简单的使用示例"""
261
+ print("连接去重服务...")
262
+ with DedupClient("localhost:50051") as client:
263
+ ns = "spider_news"
264
+ expire_at = int(time.time() * 1000) + 30 * 86400 * 1000 # 30天后
265
+
266
+ # 测试 check_and_set
267
+ print("\n--- 测试 check_and_set ---")
268
+ result = client.check_and_set(ns, b"https://example.com/1", expire_at)
269
+ print(f"第一次写入: {result}") # inserted
270
+
271
+ result = client.check_and_set(ns, b"https://example.com/1", expire_at)
272
+ print(f"第二次写入: {result}") # duplicate
273
+
274
+ # 测试 exists
275
+ print("\n--- 测试 exists ---")
276
+ print(f"存在: {client.exists(ns, b'https://example.com/1')}") # True
277
+ print(f"不存在: {client.exists(ns, b'https://example.com/999')}") # False
278
+
279
+ # 测试 get
280
+ print("\n--- 测试 get ---")
281
+ value = client.get(ns, b"https://example.com/1")
282
+ print(f"详情: {value}")
283
+
284
+ # 测试批量
285
+ print("\n--- 测试 batch_check_and_set ---")
286
+ items = [
287
+ (ns, f"https://example.com/batch_{i}".encode(), expire_at, None)
288
+ for i in range(10)
289
+ ]
290
+ results = client.batch_check_and_set(items)
291
+ print(f"批量结果: {results}")
292
+
293
+ # 测试 stats
294
+ print("\n--- 服务状态 ---")
295
+ stats = client.stats()
296
+ for k, v in stats.items():
297
+ print(f" {k}: {v}")
298
+
299
+ # 测试 delete
300
+ print("\n--- 测试 delete ---")
301
+ deleted = client.delete(ns, b"https://example.com/1")
302
+ print(f"删除: {deleted}") # True
303
+ print(f"删除后存在: {client.exists(ns, b'https://example.com/1')}") # False
304
+
305
+
306
+ if __name__ == "__main__":
307
+ main()
@@ -0,0 +1,75 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: dedup.proto
5
+ # Protobuf Python Version: 7.35.1
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ try:
13
+ _runtime_version.ValidateProtobufRuntimeVersion(
14
+ _runtime_version.Domain.PUBLIC,
15
+ 7,
16
+ 35,
17
+ 1,
18
+ '',
19
+ 'dedup.proto'
20
+ )
21
+ except Exception:
22
+ pass
23
+ # @@protoc_insertion_point(imports)
24
+
25
+ _sym_db = _symbol_database.Default()
26
+
27
+
28
+
29
+
30
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x64\x65\x64up.proto\x12\x08\x64\x65\x64up.v1\"\\\n\x12\x43heckAndSetRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x14\n\x0c\x65xpire_at_ms\x18\x03 \x01(\x04\x12\x10\n\x08metadata\x18\x04 \x01(\x0c\"a\n\x13\x43heckAndSetResponse\x12%\n\x06result\x18\x01 \x01(\x0e\x32\x15.dedup.v1.CheckResult\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.dedup.v1.DedupValue\"F\n\x17\x42\x61tchCheckAndSetRequest\x12+\n\x05items\x18\x01 \x03(\x0b\x32\x1c.dedup.v1.CheckAndSetRequest\"J\n\x18\x42\x61tchCheckAndSetResponse\x12.\n\x07results\x18\x01 \x03(\x0b\x32\x1d.dedup.v1.CheckAndSetResponse\"/\n\rExistsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x0c\" \n\x0e\x45xistsResponse\x12\x0e\n\x06\x65xists\x18\x01 \x01(\x08\"5\n\x12\x42\x61tchExistsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\x0c\"%\n\x13\x42\x61tchExistsResponse\x12\x0e\n\x06\x65xists\x18\x01 \x03(\x08\",\n\nGetRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x0c\"A\n\x0bGetResponse\x12\r\n\x05\x66ound\x18\x01 \x01(\x08\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.dedup.v1.DedupValue\"/\n\rDeleteRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x0c\"!\n\x0e\x44\x65leteResponse\x12\x0f\n\x07\x64\x65leted\x18\x01 \x01(\x08\"5\n\x12\x42\x61tchDeleteRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\x12\x0c\n\x04keys\x18\x02 \x03(\x0c\"&\n\x13\x42\x61tchDeleteResponse\x12\x0f\n\x07\x64\x65leted\x18\x01 \x03(\x08\"!\n\x0cStatsRequest\x12\x11\n\tnamespace\x18\x01 \x01(\t\"\xdb\x01\n\rStatsResponse\x12\x12\n\ntotal_keys\x18\x01 \x01(\x04\x12\x16\n\x0e\x62loom_capacity\x18\x02 \x01(\x04\x12\x18\n\x10\x62loom_insertions\x18\x03 \x01(\x04\x12\x11\n\tbloom_fpp\x18\x04 \x01(\x01\x12\x15\n\rcleaned_total\x18\x05 \x01(\x04\x12\x18\n\x10\x62\x61\x63klog_estimate\x18\x06 \x01(\x04\x12\x15\n\rcurrent_speed\x18\x07 \x01(\x04\x12\x14\n\x0cqps_estimate\x18\x08 \x01(\x04\x12\x13\n\x0b\x65rror_count\x18\t \x01(\x04\"K\n\nDedupValue\x12\x14\n\x0c\x65xpire_at_ms\x18\x01 \x01(\x04\x12\x15\n\rcreated_at_ms\x18\x02 \x01(\x04\x12\x10\n\x08metadata\x18\x03 \x01(\x0c*D\n\x0b\x43heckResult\x12\x0c\n\x08INSERTED\x10\x00\x12\r\n\tDUPLICATE\x10\x01\x12\x18\n\x14\x45XPIRED_AND_REPLACED\x10\x02\x32\xb5\x04\n\x0c\x44\x65\x64upService\x12J\n\x0b\x43heckAndSet\x12\x1c.dedup.v1.CheckAndSetRequest\x1a\x1d.dedup.v1.CheckAndSetResponse\x12Y\n\x10\x42\x61tchCheckAndSet\x12!.dedup.v1.BatchCheckAndSetRequest\x1a\".dedup.v1.BatchCheckAndSetResponse\x12;\n\x06\x45xists\x12\x17.dedup.v1.ExistsRequest\x1a\x18.dedup.v1.ExistsResponse\x12J\n\x0b\x42\x61tchExists\x12\x1c.dedup.v1.BatchExistsRequest\x1a\x1d.dedup.v1.BatchExistsResponse\x12\x32\n\x03Get\x12\x14.dedup.v1.GetRequest\x1a\x15.dedup.v1.GetResponse\x12;\n\x06\x44\x65lete\x12\x17.dedup.v1.DeleteRequest\x1a\x18.dedup.v1.DeleteResponse\x12J\n\x0b\x42\x61tchDelete\x12\x1c.dedup.v1.BatchDeleteRequest\x1a\x1d.dedup.v1.BatchDeleteResponse\x12\x38\n\x05Stats\x12\x16.dedup.v1.StatsRequest\x1a\x17.dedup.v1.StatsResponseb\x06proto3')
31
+
32
+ _globals = globals()
33
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
34
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'dedup_pb2', _globals)
35
+ if not _descriptor._USE_C_DESCRIPTORS:
36
+ DESCRIPTOR._loaded_options = None
37
+ _globals['_CHECKRESULT']._serialized_start=1169
38
+ _globals['_CHECKRESULT']._serialized_end=1237
39
+ _globals['_CHECKANDSETREQUEST']._serialized_start=25
40
+ _globals['_CHECKANDSETREQUEST']._serialized_end=117
41
+ _globals['_CHECKANDSETRESPONSE']._serialized_start=119
42
+ _globals['_CHECKANDSETRESPONSE']._serialized_end=216
43
+ _globals['_BATCHCHECKANDSETREQUEST']._serialized_start=218
44
+ _globals['_BATCHCHECKANDSETREQUEST']._serialized_end=288
45
+ _globals['_BATCHCHECKANDSETRESPONSE']._serialized_start=290
46
+ _globals['_BATCHCHECKANDSETRESPONSE']._serialized_end=364
47
+ _globals['_EXISTSREQUEST']._serialized_start=366
48
+ _globals['_EXISTSREQUEST']._serialized_end=413
49
+ _globals['_EXISTSRESPONSE']._serialized_start=415
50
+ _globals['_EXISTSRESPONSE']._serialized_end=447
51
+ _globals['_BATCHEXISTSREQUEST']._serialized_start=449
52
+ _globals['_BATCHEXISTSREQUEST']._serialized_end=502
53
+ _globals['_BATCHEXISTSRESPONSE']._serialized_start=504
54
+ _globals['_BATCHEXISTSRESPONSE']._serialized_end=541
55
+ _globals['_GETREQUEST']._serialized_start=543
56
+ _globals['_GETREQUEST']._serialized_end=587
57
+ _globals['_GETRESPONSE']._serialized_start=589
58
+ _globals['_GETRESPONSE']._serialized_end=654
59
+ _globals['_DELETEREQUEST']._serialized_start=656
60
+ _globals['_DELETEREQUEST']._serialized_end=703
61
+ _globals['_DELETERESPONSE']._serialized_start=705
62
+ _globals['_DELETERESPONSE']._serialized_end=738
63
+ _globals['_BATCHDELETEREQUEST']._serialized_start=740
64
+ _globals['_BATCHDELETEREQUEST']._serialized_end=793
65
+ _globals['_BATCHDELETERESPONSE']._serialized_start=795
66
+ _globals['_BATCHDELETERESPONSE']._serialized_end=833
67
+ _globals['_STATSREQUEST']._serialized_start=835
68
+ _globals['_STATSREQUEST']._serialized_end=868
69
+ _globals['_STATSRESPONSE']._serialized_start=871
70
+ _globals['_STATSRESPONSE']._serialized_end=1090
71
+ _globals['_DEDUPVALUE']._serialized_start=1092
72
+ _globals['_DEDUPVALUE']._serialized_end=1167
73
+ _globals['_DEDUPSERVICE']._serialized_start=1240
74
+ _globals['_DEDUPSERVICE']._serialized_end=1805
75
+ # @@protoc_insertion_point(module_scope)