wedata3-feature-engineering 0.2.5.dev3__tar.gz → 0.2.6.dev1__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.
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/PKG-INFO +3 -1
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/setup.py +2 -1
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/tests/test_cleanup_packed.py +95 -25
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/tests/test_client.py +41 -0
- wedata3_feature_engineering-0.2.6.dev1/tests/test_packed_codec.py +380 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/tests/test_redis_packed.py +145 -104
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/tests/test_sync_template.py +86 -7
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/tests/test_table_client_dispatch.py +55 -10
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/cloud_sdk_client/models.py +2 -2
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/constants/constants.py +1 -1
- wedata3_feature_engineering-0.2.6.dev1/wedata/common/online_store/__init__.py +7 -0
- wedata3_feature_engineering-0.2.6.dev1/wedata/common/online_store/packed_codec.py +653 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/__init__.py +1 -1
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/client.py +39 -12
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/feast/infra/online_store/cleanup_packed.py +82 -45
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/feast/infra/online_store/redis_packed.py +158 -197
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/table_client/table_client.py +128 -35
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata3_feature_engineering.egg-info/PKG-INFO +3 -1
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata3_feature_engineering.egg-info/SOURCES.txt +2 -1
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata3_feature_engineering.egg-info/requires.txt +2 -0
- wedata3_feature_engineering-0.2.5.dev3/tests/test_packed_codec.py +0 -291
- wedata3_feature_engineering-0.2.5.dev3/wedata/feature_engineering/feast/infra/online_store/packed_codec.py +0 -520
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/README.md +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/setup.cfg +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/tests/test_common_utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/tests/test_feast_client_validate_keys.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/tests/test_feature_spec_training_set.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/tests/test_feature_store.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/tests/test_feature_table_client_validate_keys.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/tests/test_table_client.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/base_table_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/base_table_client/base.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/cloud_sdk_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/cloud_sdk_client/client.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/cloud_sdk_client/utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/constants/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/constants/engine_types.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/column_info.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/environment_variables.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/feature.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/feature_column_info.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/feature_function.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/feature_lookup.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/feature_spec.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/feature_spec_constants.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/feature_table.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/feature_table_info.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/function_info.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/on_demand_column_info.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/source_data_column_info.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/entities/training_set.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/feast_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/feast_client/feast_client.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/log/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/log/logger.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/protos/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/protos/feature_store_pb2.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/spark_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/spark_client/spark_client.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/common_utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/env_utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/feature_lookup_utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/feature_spec_utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/feature_utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/on_demand_utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/schema_utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/signature_utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/topological_sort.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/training_set_utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/uc_utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/common/utils/validation_utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/feast/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/feast/infra/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/feast/infra/online_store/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/feast/infra/online_store/postgres.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/ml_training_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/ml_training_client/ml_training_client.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/mlflow_model.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_engineering/table_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_store/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_store/client.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_store/common/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_store/common/store_config/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_store/common/store_config/redis.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_store/constants/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_store/constants/engine_types.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_store/feature_table_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_store/feature_table_client/feature_table_client.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_store/mlflow_model.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_store/training_set_client/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/feature_store/training_set_client/training_set_client.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/tempo/__init__.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/tempo/interpol.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/tempo/intervals.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/tempo/io.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/tempo/ml.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/tempo/resample.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/tempo/tsdf.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata/tempo/utils.py +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata3_feature_engineering.egg-info/dependency_links.txt +0 -0
- {wedata3_feature_engineering-0.2.5.dev3 → wedata3_feature_engineering-0.2.6.dev1}/wedata3_feature_engineering.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wedata3-feature-engineering
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6.dev1
|
|
4
4
|
Summary: Wedata Feature Engineering Library Development
|
|
5
5
|
Home-page:
|
|
6
6
|
Author: meahqian
|
|
@@ -14,6 +14,7 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
Requires-Dist: pandas>=1.0.0
|
|
15
15
|
Requires-Dist: feast[postgres,redis]==0.49.0
|
|
16
16
|
Requires-Dist: grpcio==1.74.0
|
|
17
|
+
Requires-Dist: msgpack<2,>=1.0
|
|
17
18
|
Requires-Dist: tencentcloud-sdk-python
|
|
18
19
|
Requires-Dist: ipython
|
|
19
20
|
Requires-Dist: msgpack<2.0.0,>=1.0.0
|
|
@@ -25,6 +26,7 @@ Provides-Extra: dev
|
|
|
25
26
|
Requires-Dist: flake8; extra == "dev"
|
|
26
27
|
Requires-Dist: pytest; extra == "dev"
|
|
27
28
|
Requires-Dist: python-dotenv; extra == "dev"
|
|
29
|
+
Requires-Dist: fakeredis[lua]; extra == "dev"
|
|
28
30
|
Dynamic: author
|
|
29
31
|
Dynamic: classifier
|
|
30
32
|
Dynamic: description
|
|
@@ -13,6 +13,7 @@ setup(
|
|
|
13
13
|
install_requires=[
|
|
14
14
|
'pandas>=1.0.0',
|
|
15
15
|
'feast[redis,postgres]==0.49.0', 'grpcio==1.74.0',
|
|
16
|
+
'msgpack>=1.0,<2',
|
|
16
17
|
'tencentcloud-sdk-python',
|
|
17
18
|
'ipython',
|
|
18
19
|
# Redis 整行打包在线存储(见 doc/plan/redis-packed-codec-design.md)
|
|
@@ -21,7 +22,7 @@ setup(
|
|
|
21
22
|
extras_require={
|
|
22
23
|
'mlflow2': ['mlflow==2.17.2',],
|
|
23
24
|
'mlflow3': ['mlflow>=3.10.0,<3.11.0'],
|
|
24
|
-
'dev': ['flake8', 'pytest', 'python-dotenv'],
|
|
25
|
+
'dev': ['flake8', 'pytest', 'python-dotenv', 'fakeredis[lua]'],
|
|
25
26
|
},
|
|
26
27
|
python_requires='>=3.10',
|
|
27
28
|
author="meahqian",
|
|
@@ -8,6 +8,9 @@ cleanup_packed 单元测试(fakeredis)。
|
|
|
8
8
|
STALE/REJECTED 删、超时 WRITING 转 ABANDONED 删、PURGED 条目过期删
|
|
9
9
|
3. 孤儿 __meta__ 清理
|
|
10
10
|
4. purge_batch 幂等、extend_active_ttl 应急路径
|
|
11
|
+
5. drop_all 整表删除:控制 key 与数据 key 都删,不碰其他表
|
|
12
|
+
|
|
13
|
+
key 格式与 BatchInfo 统一走 common codec(与推理侧同一份实现)。
|
|
11
14
|
"""
|
|
12
15
|
|
|
13
16
|
import time
|
|
@@ -15,10 +18,27 @@ import time
|
|
|
15
18
|
import fakeredis
|
|
16
19
|
import pytest
|
|
17
20
|
|
|
18
|
-
from wedata.
|
|
21
|
+
from wedata.common.online_store import packed_codec as pc
|
|
22
|
+
from wedata.common.online_store.packed_codec import SchemaMeta
|
|
19
23
|
from wedata.feature_engineering.feast.infra.online_store import cleanup_packed as cp
|
|
20
24
|
|
|
21
|
-
PREFIX = "1001:
|
|
25
|
+
PREFIX = "1001:ab23cd45efgh"
|
|
26
|
+
|
|
27
|
+
META = SchemaMeta(
|
|
28
|
+
codec_version=pc.CODEC_VERSION,
|
|
29
|
+
schema_version="a3f2c1d8",
|
|
30
|
+
fields=(("a", "INT64"),),
|
|
31
|
+
entity_keys=("u",),
|
|
32
|
+
created_at=0,
|
|
33
|
+
source_table="cat.db.t",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def all_keys(client, prefix=PREFIX):
|
|
38
|
+
keys = set()
|
|
39
|
+
for pattern in pc.table_key_patterns(prefix):
|
|
40
|
+
keys.update(k.decode() if isinstance(k, bytes) else k for k in client.scan_iter(match=pattern))
|
|
41
|
+
return sorted(keys)
|
|
22
42
|
|
|
23
43
|
|
|
24
44
|
def make_client():
|
|
@@ -29,20 +49,15 @@ def register(client, prefix, sv, dv, state, watermark=1000, run_start=None, upda
|
|
|
29
49
|
"""登记一个批次并可选写两个数据 key。"""
|
|
30
50
|
member = pc.build_batch_member(sv, dv)
|
|
31
51
|
info_raw = pc.build_batch_info(
|
|
32
|
-
|
|
52
|
+
watermark=watermark,
|
|
33
53
|
run_start_ms=run_start if run_start is not None else int(time.time() * 1000),
|
|
34
54
|
state=state,
|
|
35
|
-
|
|
55
|
+
updated_at=updated_at if updated_at is not None else int(time.time()),
|
|
56
|
+
retired_at=retired_at,
|
|
36
57
|
)
|
|
37
|
-
# build_batch_info 无 retired_at 槽位,retired 状态在 hash 里补
|
|
38
|
-
if retired_at is not None:
|
|
39
|
-
import json
|
|
40
|
-
info = json.loads(info_raw)
|
|
41
|
-
info["retired_at"] = retired_at
|
|
42
|
-
info_raw = json.dumps(info, separators=(",", ":"))
|
|
43
58
|
client.hset(pc.versions_key(prefix), member, info_raw)
|
|
44
|
-
client.set(pc.data_key(prefix, sv, dv, "u1"), pc.encode_row(
|
|
45
|
-
client.set(pc.data_key(prefix, sv, dv, "u2"), pc.encode_row(
|
|
59
|
+
client.set(pc.data_key(prefix, sv, dv, "u1"), pc.encode_row(META, 1, {"a": 1}))
|
|
60
|
+
client.set(pc.data_key(prefix, sv, dv, "u2"), pc.encode_row(META, 2, {"a": 2}))
|
|
46
61
|
return member
|
|
47
62
|
|
|
48
63
|
|
|
@@ -73,17 +88,15 @@ class TestPrecheck:
|
|
|
73
88
|
client = make_client()
|
|
74
89
|
register(client, PREFIX, sv(1), dv(1), pc.STATE_STALE)
|
|
75
90
|
cp.precheck_and_purge(client, PREFIX)
|
|
76
|
-
# STALE
|
|
77
|
-
|
|
78
|
-
for k in client.scan_iter(match=f"{PREFIX}:*"))
|
|
79
|
-
assert remain == [pc.versions_key(PREFIX)]
|
|
91
|
+
# STALE 批次数据已被清掉,只剩登记表
|
|
92
|
+
assert all_keys(client) == [pc.versions_key(PREFIX)]
|
|
80
93
|
|
|
81
94
|
def test_abandoned_writing_purged(self):
|
|
82
95
|
client = make_client()
|
|
83
96
|
# updated_at 设为 7 小时前
|
|
84
97
|
register(client, PREFIX, sv(1), dv(1), pc.STATE_WRITING, updated_at=int(time.time()) - 7 * 3600)
|
|
85
98
|
cp.precheck_and_purge(client, PREFIX)
|
|
86
|
-
info = pc.decode_batch_info(client.hget(pc.versions_key(PREFIX), pc.build_batch_member(sv(1), dv(1)))
|
|
99
|
+
info = pc.decode_batch_info(client.hget(pc.versions_key(PREFIX), pc.build_batch_member(sv(1), dv(1))))
|
|
87
100
|
assert info[pc.BATCH_STATE] == pc.STATE_PURGED
|
|
88
101
|
assert not client.exists(pc.data_key(PREFIX, sv(1), dv(1), "u1"))
|
|
89
102
|
|
|
@@ -103,7 +116,7 @@ class TestCleanupRules:
|
|
|
103
116
|
client, active = self._setup()
|
|
104
117
|
cp.cleanup(client, PREFIX)
|
|
105
118
|
assert client.exists(pc.data_key(PREFIX, sv(1), dv(1), "u1"))
|
|
106
|
-
info = pc.decode_batch_info(client.hget(pc.versions_key(PREFIX), active)
|
|
119
|
+
info = pc.decode_batch_info(client.hget(pc.versions_key(PREFIX), active))
|
|
107
120
|
assert info[pc.BATCH_STATE] == pc.STATE_ACTIVE
|
|
108
121
|
|
|
109
122
|
def test_newest_retired_kept_old_retired_deleted(self):
|
|
@@ -139,19 +152,27 @@ class TestCleanupRules:
|
|
|
139
152
|
cp.cleanup(client, PREFIX)
|
|
140
153
|
assert client.hget(pc.versions_key(PREFIX), member) is None # 登记条目也删了
|
|
141
154
|
|
|
142
|
-
def
|
|
155
|
+
def test_corrupted_info_treated_as_abandoned_and_purged(self):
|
|
156
|
+
client, active = self._setup()
|
|
157
|
+
member = register(client, PREFIX, sv(2), dv(2), pc.STATE_WRITING)
|
|
158
|
+
client.hset(pc.versions_key(PREFIX), member, "not-json")
|
|
159
|
+
assert cp.cleanup(client, PREFIX) == 1
|
|
160
|
+
assert not client.exists(pc.data_key(PREFIX, sv(2), dv(2), "u1"))
|
|
161
|
+
info = pc.decode_batch_info(client.hget(pc.versions_key(PREFIX), member))
|
|
162
|
+
assert info[pc.BATCH_STATE] == pc.STATE_PURGED
|
|
163
|
+
|
|
164
|
+
def test_malformed_member_dropped_without_blocking(self):
|
|
143
165
|
client, active = self._setup()
|
|
144
166
|
client.hset(pc.versions_key(PREFIX), "garbage-member", "not-json")
|
|
145
|
-
|
|
146
|
-
assert
|
|
147
|
-
assert client.hget(pc.versions_key(PREFIX), "garbage-member") is not None # 无法解析,原样保留
|
|
167
|
+
cp.cleanup(client, PREFIX) # 不抛错,门禁不会被脏条目卡死
|
|
168
|
+
assert client.hget(pc.versions_key(PREFIX), "garbage-member") is None
|
|
148
169
|
|
|
149
170
|
def test_orphan_meta_removed(self):
|
|
150
171
|
client, active = self._setup()
|
|
151
172
|
# ACTIVE 批次的 meta 正常存在
|
|
152
|
-
client.set(pc.meta_key(PREFIX, sv(1)),
|
|
173
|
+
client.set(pc.meta_key(PREFIX, sv(1)), META.to_bytes())
|
|
153
174
|
# sv=99 的 meta 存在但无任何批次
|
|
154
|
-
client.set(pc.meta_key(PREFIX, sv(99)),
|
|
175
|
+
client.set(pc.meta_key(PREFIX, sv(99)), META.to_bytes())
|
|
155
176
|
cp.cleanup(client, PREFIX)
|
|
156
177
|
assert not client.exists(pc.meta_key(PREFIX, sv(99)))
|
|
157
178
|
# active 批次的 meta 保留
|
|
@@ -169,7 +190,7 @@ class TestUtilities:
|
|
|
169
190
|
cp.purge_batch(client, PREFIX, member)
|
|
170
191
|
assert not client.exists(pc.data_key(PREFIX, sv(1), dv(1), "u1"))
|
|
171
192
|
cp.purge_batch(client, PREFIX, member) # 再跑一次不抛错
|
|
172
|
-
info = pc.decode_batch_info(client.hget(pc.versions_key(PREFIX), member)
|
|
193
|
+
info = pc.decode_batch_info(client.hget(pc.versions_key(PREFIX), member))
|
|
173
194
|
assert info[pc.BATCH_STATE] == pc.STATE_PURGED
|
|
174
195
|
|
|
175
196
|
def test_purge_does_not_touch_other_batches(self):
|
|
@@ -193,3 +214,52 @@ class TestUtilities:
|
|
|
193
214
|
client = make_client()
|
|
194
215
|
with pytest.raises(cp.CleanupError):
|
|
195
216
|
cp.extend_active_ttl(client, PREFIX, 3600)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# drop_all(删除在线表的清理作业)
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
class TestDropAll:
|
|
224
|
+
def _populate(self, client, prefix):
|
|
225
|
+
member = register(client, prefix, sv(1), dv(1), pc.STATE_ACTIVE)
|
|
226
|
+
client.set(pc.active_key(prefix), member)
|
|
227
|
+
client.set(pc.meta_key(prefix, sv(1)), META.to_bytes())
|
|
228
|
+
|
|
229
|
+
def test_removes_control_and_data_keys(self):
|
|
230
|
+
client = make_client()
|
|
231
|
+
self._populate(client, PREFIX)
|
|
232
|
+
assert len(all_keys(client)) == 5 # active / versions / meta / 2 条数据
|
|
233
|
+
assert cp.drop_all(client, PREFIX) == 5
|
|
234
|
+
assert all_keys(client) == []
|
|
235
|
+
assert cp.drop_all(client, PREFIX) == 0 # 幂等
|
|
236
|
+
|
|
237
|
+
def test_other_tables_untouched(self):
|
|
238
|
+
client = make_client()
|
|
239
|
+
self._populate(client, PREFIX)
|
|
240
|
+
for other in ("1001:ab12cd34ef0", "1002:ab12cd34ef"):
|
|
241
|
+
self._populate(client, other)
|
|
242
|
+
cp.drop_all(client, PREFIX)
|
|
243
|
+
assert all_keys(client, "1001:ab12cd34ef0") and all_keys(client, "1002:ab12cd34ef")
|
|
244
|
+
|
|
245
|
+
def test_malformed_prefix_rejected(self):
|
|
246
|
+
"""防呆:畸形前缀(裸 workspaceId / 短哈希)拒绝执行,SCAN 模式会跨表清库。"""
|
|
247
|
+
import pytest
|
|
248
|
+
|
|
249
|
+
client = make_client()
|
|
250
|
+
self._populate(client, PREFIX)
|
|
251
|
+
for bad in ("1001", "1001:", "1001:ab12cd34ef", "1001:ab12cd34ef0", "vpc-1001:ab12cd34efgh"):
|
|
252
|
+
with pytest.raises(cp.CleanupError):
|
|
253
|
+
cp.drop_all(client, bad)
|
|
254
|
+
assert len(all_keys(client)) == 5 # 一个 key 都没动
|
|
255
|
+
|
|
256
|
+
def test_redis_error_propagates(self, monkeypatch):
|
|
257
|
+
client = make_client()
|
|
258
|
+
self._populate(client, PREFIX)
|
|
259
|
+
|
|
260
|
+
def boom(*args, **kwargs):
|
|
261
|
+
raise cp.redis.ConnectionError("down")
|
|
262
|
+
|
|
263
|
+
monkeypatch.setattr(client, "unlink", boom)
|
|
264
|
+
with pytest.raises(cp.redis.ConnectionError):
|
|
265
|
+
cp.drop_all(client, PREFIX)
|
|
@@ -1625,6 +1625,47 @@ class TestOnlineTableOperations:
|
|
|
1625
1625
|
assert call_kwargs["trigger"] is mock_trigger
|
|
1626
1626
|
assert call_kwargs["online_catalog_name"] == "online_cat"
|
|
1627
1627
|
|
|
1628
|
+
def test_publish_table_redis_without_online_names(self):
|
|
1629
|
+
"""Redis 发布:只传离线表与类型,在线库表名保持 None 透传给内部客户端校验"""
|
|
1630
|
+
client = _make_client()
|
|
1631
|
+
client.publish_table(
|
|
1632
|
+
catalog_name="cat", schema_name="schema", table_name="tbl", online_store_type="REDIS"
|
|
1633
|
+
)
|
|
1634
|
+
client._feature_table_client.publish_table.assert_called_once_with(
|
|
1635
|
+
catalog_name="cat",
|
|
1636
|
+
schema_name="schema",
|
|
1637
|
+
table_name="tbl",
|
|
1638
|
+
online_db_name=None,
|
|
1639
|
+
online_table_name=None,
|
|
1640
|
+
online_catalog_name=None,
|
|
1641
|
+
trigger=None,
|
|
1642
|
+
online_store_type="REDIS",
|
|
1643
|
+
)
|
|
1644
|
+
|
|
1645
|
+
def test_read_table_online_delegates(self):
|
|
1646
|
+
"""read_table(is_online=True) 透传在线读取参数"""
|
|
1647
|
+
client = _make_client()
|
|
1648
|
+
client._parse_table_name = lambda n, d, c: (n, d, c)
|
|
1649
|
+
rows = [{"member_id": "10086"}]
|
|
1650
|
+
client.read_table("tbl", "db", "cat", is_online=True, entity_rows=rows, redis_connection_string="h:6379")
|
|
1651
|
+
client._feature_table_client.read_table.assert_called_once_with(
|
|
1652
|
+
name="tbl",
|
|
1653
|
+
database_name="db",
|
|
1654
|
+
catalog_name="cat",
|
|
1655
|
+
is_online=True,
|
|
1656
|
+
entity_rows=rows,
|
|
1657
|
+
redis_connection_string="h:6379",
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
def test_read_table_offline_call_unchanged(self):
|
|
1661
|
+
"""默认离线读取:调用参数与改造前一致"""
|
|
1662
|
+
client = _make_client()
|
|
1663
|
+
client._parse_table_name = lambda n, d, c: (n, d, c)
|
|
1664
|
+
client.read_table("tbl", "db", "cat")
|
|
1665
|
+
client._feature_table_client.read_table.assert_called_once_with(
|
|
1666
|
+
name="tbl", database_name="db", catalog_name="cat"
|
|
1667
|
+
)
|
|
1668
|
+
|
|
1628
1669
|
def test_drop_online_table_delegates(self):
|
|
1629
1670
|
"""drop_online_table 应委托给 _feature_table_client.drop_online_table"""
|
|
1630
1671
|
client = _make_client()
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import msgpack
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from wedata.common.online_store import packed_codec as pc
|
|
14
|
+
from wedata.common.online_store.packed_codec import (
|
|
15
|
+
PackedCodecError,
|
|
16
|
+
SchemaMeta,
|
|
17
|
+
UnsupportedCodecVersion,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
PREFIX = "17893778831981982:ie3ton4o6vrh"
|
|
21
|
+
FIELDS = (("age", "INT64"), ("city_id", "INT32"), ("ctr_7d", "DOUBLE"), ("name", "STRING"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_meta(fields=FIELDS, entity_keys=("customer_id",)):
|
|
25
|
+
return SchemaMeta(
|
|
26
|
+
codec_version=pc.CODEC_VERSION,
|
|
27
|
+
schema_version=pc.compute_schema_version(fields, entity_keys),
|
|
28
|
+
fields=tuple(fields),
|
|
29
|
+
entity_keys=tuple(entity_keys),
|
|
30
|
+
created_at=1789892926028,
|
|
31
|
+
source_table="prod_cat.dws.member_offline_feat",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------- key(§2.6)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_key_examples_match_doc():
|
|
39
|
+
assert pc.active_key(PREFIX) == "{17893778831981982:ie3ton4o6vrh}:__active__"
|
|
40
|
+
assert pc.meta_key(PREFIX, "a3f2c1d8") == "{17893778831981982:ie3ton4o6vrh}:__meta__:a3f2c1d8"
|
|
41
|
+
assert pc.versions_key(PREFIX) == "{17893778831981982:ie3ton4o6vrh}:__versions__"
|
|
42
|
+
key = pc.data_key(PREFIX, "a3f2c1d8", "mu9k18p8a7", "1000000123")
|
|
43
|
+
assert key == "17893778831981982:ie3ton4o6vrh:a3f2c1d8:mu9k18p8a7:1000000123"
|
|
44
|
+
assert len(key.encode()) == 61
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_control_keys_share_slot_and_data_key_has_no_braces():
|
|
48
|
+
from redis.crc import key_slot
|
|
49
|
+
|
|
50
|
+
slots = {key_slot(k.encode()) for k in (
|
|
51
|
+
pc.active_key(PREFIX), pc.meta_key(PREFIX, "a3f2c1d8"), pc.versions_key(PREFIX))}
|
|
52
|
+
assert len(slots) == 1
|
|
53
|
+
key = pc.data_key(PREFIX, "a3f2c1d8", "mu9k18p8a7", pc.build_entity_key_from_values(["{x}"]))
|
|
54
|
+
assert "{" not in key and "}" not in key
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_data_key_rejects_unescaped_braces_and_bad_prefix():
|
|
58
|
+
with pytest.raises(PackedCodecError):
|
|
59
|
+
pc.data_key(PREFIX, "a3f2c1d8", "mu9k18p8a7", "{grp}1")
|
|
60
|
+
with pytest.raises(PackedCodecError):
|
|
61
|
+
pc.active_key("{ws}:tid")
|
|
62
|
+
with pytest.raises(PackedCodecError):
|
|
63
|
+
pc.data_key_prefix(PREFIX, "a3:f2", "mu9k18p8a7")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------- ek(§2.6.3)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_entity_key_separator_and_percent_no_collision():
|
|
70
|
+
assert pc.build_entity_key_from_values(["a|b", "c"]) != pc.build_entity_key_from_values(["a", "b|c"])
|
|
71
|
+
assert pc.build_entity_key_from_values(["%"]) != pc.build_entity_key_from_values(["%25"])
|
|
72
|
+
assert pc.build_entity_key_from_values(["|"]) != pc.build_entity_key_from_values(["%7C"])
|
|
73
|
+
assert pc.build_entity_key_from_values(["shopA", 2026]) == "shopA|2026"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_entity_key_braces_spread_over_slots():
|
|
77
|
+
from redis.crc import key_slot
|
|
78
|
+
|
|
79
|
+
base = pc.data_key_prefix(PREFIX, "a3f2c1d8", "mu9k18p8a7")
|
|
80
|
+
slots = {key_slot((base + pc.build_entity_key_from_values([f"{{grp}}{i}"])).encode()) for i in range(10000)}
|
|
81
|
+
assert len(slots) > 7000
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_entity_key_random_inputs_never_contain_braces():
|
|
85
|
+
rnd = random.Random(7)
|
|
86
|
+
alphabet = "{}|%:ab1 "
|
|
87
|
+
for _ in range(2000):
|
|
88
|
+
vals = ["".join(rnd.choice(alphabet) for _ in range(rnd.randint(1, 8))) for _ in range(rnd.randint(1, 3))]
|
|
89
|
+
key = pc.data_key(PREFIX, "a3f2c1d8", "mu9k18p8a7", pc.build_entity_key_from_values(vals))
|
|
90
|
+
assert "{" not in key and "}" not in key
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_entity_value_normalization():
|
|
94
|
+
assert pc.entity_value_to_str(1000000123) == "1000000123"
|
|
95
|
+
assert pc.entity_value_to_str(1000000123.0) == "1000000123"
|
|
96
|
+
assert pc.entity_value_to_str(True) == "true"
|
|
97
|
+
assert pc.entity_value_to_str(1.5) == "1.5"
|
|
98
|
+
for bad in (None, float("nan"), b"x", Decimal("NaN"), object()):
|
|
99
|
+
with pytest.raises(PackedCodecError):
|
|
100
|
+
pc.entity_value_to_str(bad)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_entity_value_normalization_decimal():
|
|
104
|
+
assert pc.entity_value_to_str(Decimal("123")) == "123"
|
|
105
|
+
assert pc.entity_value_to_str(Decimal("123.00")) == pc.entity_value_to_str(123) == "123"
|
|
106
|
+
assert pc.entity_value_to_str(Decimal("1E+2")) == "100"
|
|
107
|
+
assert pc.entity_value_to_str(Decimal("1.50")) == pc.entity_value_to_str(1.5) == "1.5"
|
|
108
|
+
assert pc.entity_value_to_str(Decimal("-0.25")) == pc.entity_value_to_str(-0.25)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_entity_value_normalization_datetime():
|
|
112
|
+
assert pc.entity_value_to_str(dt.date(2026, 1, 2)) == "2026-01-02"
|
|
113
|
+
assert pc.entity_value_to_str(dt.datetime(2026, 1, 2, 3, 4, 5)) == "2026-01-02 03:04:05"
|
|
114
|
+
assert pc.entity_value_to_str(dt.datetime(2026, 1, 2, 3, 4, 5, 120000)) == "2026-01-02 03:04:05.12"
|
|
115
|
+
cst = dt.timezone(dt.timedelta(hours=8))
|
|
116
|
+
assert pc.entity_value_to_str(dt.datetime(2026, 1, 2, 8, 0, 0, tzinfo=cst)) == "2026-01-02 00:00:00"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_entity_value_normalization_numpy_and_pandas():
|
|
120
|
+
np = pytest.importorskip("numpy")
|
|
121
|
+
pd = pytest.importorskip("pandas")
|
|
122
|
+
assert pc.entity_value_to_str(np.int64(7)) == "7"
|
|
123
|
+
assert pc.entity_value_to_str(np.float64(7.0)) == "7"
|
|
124
|
+
assert pc.entity_value_to_str(np.bool_(True)) == "true"
|
|
125
|
+
ts = "2026-01-02T03:04:05.123456789"
|
|
126
|
+
# datetime64[ns].item() 是纳秒整数,必须按时间而不是整数规范化
|
|
127
|
+
assert pc.entity_value_to_str(np.datetime64(ts)) == "2026-01-02 03:04:05.123456"
|
|
128
|
+
assert pc.entity_value_to_str(pd.Timestamp("2026-01-02 03:04:05")) == "2026-01-02 03:04:05"
|
|
129
|
+
with pytest.raises(PackedCodecError):
|
|
130
|
+
pc.entity_value_to_str(pd.NaT)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_build_entity_key_uses_meta_order():
|
|
134
|
+
meta = make_meta(entity_keys=("shop_id", "dt"))
|
|
135
|
+
assert pc.build_entity_key(meta, {"dt": 2026, "shop_id": "A"}) == "A|2026"
|
|
136
|
+
with pytest.raises(PackedCodecError):
|
|
137
|
+
pc.build_entity_key(meta, {"shop_id": "A"})
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------- 编解码(§3)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_encode_decode_roundtrip():
|
|
144
|
+
meta = make_meta()
|
|
145
|
+
values = {"age": -5, "city_id": 0, "ctr_7d": 0.25, "name": ""}
|
|
146
|
+
ts, out = pc.decode_row(meta, pc.encode_row(meta, 123, values))
|
|
147
|
+
assert ts == 123 and out == values
|
|
148
|
+
big = {"age": 2**63 - 1}
|
|
149
|
+
assert pc.decode_row(meta, pc.encode_row(meta, None, big))[1] == big
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_nil_not_in_result_and_requested_projection():
|
|
153
|
+
meta = make_meta()
|
|
154
|
+
raw = pc.encode_row(meta, 1, {"age": None, "ctr_7d": float("nan"), "name": "x"})
|
|
155
|
+
_, out = pc.decode_row(meta, raw)
|
|
156
|
+
assert out == {"name": "x"}
|
|
157
|
+
assert pc.decode_row(meta, raw, requested=["name", "age"])[1] == {"name": "x"}
|
|
158
|
+
with pytest.raises(PackedCodecError):
|
|
159
|
+
pc.decode_row(meta, raw, requested=["missing"])
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_length_tolerance():
|
|
163
|
+
meta = make_meta()
|
|
164
|
+
short = msgpack.packb([1, 7], use_bin_type=True)
|
|
165
|
+
assert pc.decode_row(meta, short)[1] == {"age": 7}
|
|
166
|
+
long = msgpack.packb([1, 1, 2, 3.0, "n", "extra"], use_bin_type=True)
|
|
167
|
+
assert pc.decode_row(meta, long)[1] == {"age": 1, "city_id": 2, "ctr_7d": 3.0, "name": "n"}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_meta_roundtrip_and_codec_version_guard():
|
|
171
|
+
meta = make_meta()
|
|
172
|
+
assert SchemaMeta.from_bytes(meta.to_bytes()) == meta
|
|
173
|
+
newer = dict(meta.to_dict(), codec_version=pc.CODEC_VERSION + 1)
|
|
174
|
+
with pytest.raises(UnsupportedCodecVersion):
|
|
175
|
+
SchemaMeta.from_bytes(msgpack.packb(newer, use_bin_type=True))
|
|
176
|
+
with pytest.raises(PackedCodecError):
|
|
177
|
+
SchemaMeta.from_bytes(b"\xc1")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_validate_string_lengths():
|
|
181
|
+
pc.validate_string_lengths({"a": "x" * 64})
|
|
182
|
+
with pytest.raises(PackedCodecError):
|
|
183
|
+
pc.validate_string_lengths({"a": "x" * 65})
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------- 版本号(§4 / §2.2.2)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_schema_version_order_sensitive_and_stable():
|
|
190
|
+
v1 = pc.compute_schema_version(FIELDS, ["customer_id"])
|
|
191
|
+
assert v1 == pc.compute_schema_version(list(FIELDS), ("customer_id",))
|
|
192
|
+
assert len(v1) == 8
|
|
193
|
+
assert v1 != pc.compute_schema_version(tuple(reversed(FIELDS)), ["customer_id"])
|
|
194
|
+
assert v1 != pc.compute_schema_version(FIELDS, ["user_id"])
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_schema_version_stable_across_processes_with_random_hash_seed():
|
|
198
|
+
code = (
|
|
199
|
+
"from wedata.common.online_store.packed_codec import compute_schema_version as c;"
|
|
200
|
+
"print(c([('age','INT64'),('ctr','DOUBLE')],['customer_id']))"
|
|
201
|
+
)
|
|
202
|
+
root = Path(__file__).resolve().parents[1]
|
|
203
|
+
outs = set()
|
|
204
|
+
for seed in ("1", "2", "random"):
|
|
205
|
+
env = dict(os.environ, PYTHONHASHSEED=seed, PYTHONPATH=str(root))
|
|
206
|
+
outs.add(subprocess.check_output([sys.executable, "-c", code], env=env, text=True).strip())
|
|
207
|
+
assert len(outs) == 1
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_new_data_version_format():
|
|
211
|
+
assert pc.new_data_version(1789892926028, "a7") == "mu9k18p8a7"
|
|
212
|
+
dv = pc.new_data_version(1789892926028)
|
|
213
|
+
assert len(dv) == 10 and dv.startswith("mu9k18p8")
|
|
214
|
+
assert pc.new_data_version(1, "00") == "0000000100"
|
|
215
|
+
with pytest.raises(ValueError):
|
|
216
|
+
pc.new_data_version(1, "A!")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_parse_active_pointer():
|
|
220
|
+
assert pc.parse_active_pointer(b"a3f2c1d8:mu9k18p8a7") == ("a3f2c1d8", "mu9k18p8a7")
|
|
221
|
+
for bad in ("", "abc", "a:b:c", ":b", None):
|
|
222
|
+
with pytest.raises(PackedCodecError):
|
|
223
|
+
pc.parse_active_pointer(bad)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------- feast 适配
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_build_meta_from_feature_view_keeps_features_order():
|
|
230
|
+
from feast import Entity, FeatureView, Field, FileSource, ValueType
|
|
231
|
+
from feast.types import Float64, Int64, String
|
|
232
|
+
|
|
233
|
+
fv = FeatureView(
|
|
234
|
+
name="fv",
|
|
235
|
+
entities=[Entity(name="customer_id", join_keys=["customer_id"], value_type=ValueType.INT64)],
|
|
236
|
+
schema=[Field(name="customer_id", dtype=Int64), Field(name="z", dtype=String),
|
|
237
|
+
Field(name="a", dtype=Float64), Field(name="m", dtype=Int64)],
|
|
238
|
+
source=FileSource(path="x.parquet", timestamp_field="ts"),
|
|
239
|
+
)
|
|
240
|
+
meta = pc.build_meta_from_feature_view(fv, "c.s.t", created_at=1)
|
|
241
|
+
assert meta.fields == (("z", "STRING"), ("a", "DOUBLE"), ("m", "INT64"))
|
|
242
|
+
assert meta.entity_keys == ("customer_id",)
|
|
243
|
+
assert meta.schema_version == pc.compute_schema_version(meta.fields, meta.entity_keys)
|
|
244
|
+
protos = pc.to_value_protos(meta, {"z": "s", "a": 1.5, "m": 3})
|
|
245
|
+
assert protos["z"].string_val == "s" and protos["a"].double_val == 1.5 and protos["m"].int64_val == 3
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ---------------------------------------------------------------- CAS Lua(§5.2)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_activate_lua_identical_to_verified_script():
|
|
252
|
+
script = Path(__file__).resolve().parents[3] / "science/doc/plan/scripts/active_cas_verify.py"
|
|
253
|
+
if not script.exists():
|
|
254
|
+
pytest.skip("验证脚本不在当前检出中")
|
|
255
|
+
src = script.read_text(encoding="utf-8")
|
|
256
|
+
start = src.index('ACTIVATE_LUA = r"""') + len('ACTIVATE_LUA = r"""')
|
|
257
|
+
assert src[start:src.index('"""', start)] == pc.ACTIVATE_LUA
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _lua_env():
|
|
261
|
+
fakeredis = pytest.importorskip("fakeredis")
|
|
262
|
+
pytest.importorskip("lupa")
|
|
263
|
+
r = fakeredis.FakeRedis(decode_responses=True)
|
|
264
|
+
return r, r.register_script(pc.ACTIVATE_LUA)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _register(r, member, watermark, run_start):
|
|
268
|
+
r.hset(pc.versions_key(PREFIX), member, json.dumps(
|
|
269
|
+
{"state": "WRITING", "watermark": watermark, "run_start": run_start}))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _activate(script, member):
|
|
273
|
+
sw, prev, reason = script(keys=[pc.active_key(PREFIX), pc.versions_key(PREFIX)], args=[member, 0])
|
|
274
|
+
return int(sw), prev, reason
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _state(r, member):
|
|
278
|
+
return json.loads(r.hget(pc.versions_key(PREFIX), member))["state"]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_activate_lua_scenarios():
|
|
282
|
+
r, s = _lua_env()
|
|
283
|
+
active = pc.active_key(PREFIX)
|
|
284
|
+
_register(r, "sv:T", 100, 1000)
|
|
285
|
+
assert _activate(s, "sv:T")[0] == 1 and _state(r, "sv:T") == "ACTIVE"
|
|
286
|
+
_register(r, "sv:T1", 200, 2000)
|
|
287
|
+
assert _activate(s, "sv:T1")[0] == 1 and _state(r, "sv:T") == "RETIRED"
|
|
288
|
+
_register(r, "sv:retry", 150, 3000)
|
|
289
|
+
assert _activate(s, "sv:retry")[2] == "STALE" and r.get(active) == "sv:T1"
|
|
290
|
+
_register(r, "sv:rerun", 200, 2500)
|
|
291
|
+
assert _activate(s, "sv:rerun")[0] == 1
|
|
292
|
+
_register(r, "sv:early", 200, 2400)
|
|
293
|
+
assert _activate(s, "sv:early")[2] == "STALE"
|
|
294
|
+
assert _activate(s, "sv:ghost")[2] == "NEW_NOT_REGISTERED" and r.get(active) == "sv:rerun"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def test_activate_lua_random_order_always_latest():
|
|
298
|
+
rnd = random.Random(11)
|
|
299
|
+
for _ in range(200):
|
|
300
|
+
r, s = _lua_env()
|
|
301
|
+
batches = [(f"sv:{i}", rnd.randint(1, 50), rnd.randint(1, 10_000)) for i in range(6)]
|
|
302
|
+
for m, wm, rs in batches:
|
|
303
|
+
_register(r, m, wm, rs)
|
|
304
|
+
for m, _, _ in rnd.sample(batches, len(batches)):
|
|
305
|
+
_activate(s, m)
|
|
306
|
+
expected = max(batches, key=lambda b: (b[1], b[2]))[0]
|
|
307
|
+
assert r.get(pc.active_key(PREFIX)) == expected
|
|
308
|
+
assert [m for m, _, _ in batches if _state(r, m) == "ACTIVE"] == [expected]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ---------------------------------------------------------------- 批次登记(§2.4)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_batch_member_same_shape_as_active_pointer():
|
|
315
|
+
member = pc.build_batch_member("a3f2c1d8", "mu9k18p8a7")
|
|
316
|
+
assert member == "a3f2c1d8:mu9k18p8a7"
|
|
317
|
+
assert pc.parse_batch_member(member) == pc.parse_active_pointer(member)
|
|
318
|
+
for bad in ("", "a:b", "x{y"):
|
|
319
|
+
with pytest.raises(PackedCodecError):
|
|
320
|
+
pc.build_batch_member(bad, "mu9k18p8a7")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_batch_info_roundtrip_keeps_extra_fields():
|
|
324
|
+
raw = pc.build_batch_info(
|
|
325
|
+
watermark=1760227200, run_start_ms=1760245200123, updated_at=1, rows_expected=10, exec_id=None
|
|
326
|
+
)
|
|
327
|
+
info = pc.decode_batch_info(raw.encode())
|
|
328
|
+
assert info == {
|
|
329
|
+
"state": "WRITING",
|
|
330
|
+
"watermark": 1760227200,
|
|
331
|
+
"run_start": 1760245200123,
|
|
332
|
+
"updated_at": 1,
|
|
333
|
+
"rows_expected": 10,
|
|
334
|
+
}
|
|
335
|
+
info["state"] = pc.STATE_PURGED
|
|
336
|
+
assert pc.decode_batch_info(pc.encode_batch_info(info))["rows_expected"] == 10
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@pytest.mark.parametrize(
|
|
340
|
+
"raw", ["", "not json", "[]", '{"state":"WRITING","watermark":1}', '{"watermark":1,"run_start":1}']
|
|
341
|
+
)
|
|
342
|
+
def test_decode_batch_info_rejects_corrupted(raw):
|
|
343
|
+
with pytest.raises(PackedCodecError):
|
|
344
|
+
pc.decode_batch_info(raw)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_encode_batch_info_rejects_unknown_state():
|
|
348
|
+
with pytest.raises(PackedCodecError):
|
|
349
|
+
pc.build_batch_info(watermark=1, run_start_ms=1, state="DONE")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_batch_info_works_with_activate_lua():
|
|
353
|
+
r, s = _lua_env()
|
|
354
|
+
old = pc.build_batch_member("a3f2c1d8", "aaaaaaaaaa")
|
|
355
|
+
new = pc.build_batch_member("a3f2c1d8", "bbbbbbbbbb")
|
|
356
|
+
r.hset(pc.versions_key(PREFIX), old, pc.build_batch_info(watermark=100, run_start_ms=1, rows_expected=5))
|
|
357
|
+
r.hset(pc.versions_key(PREFIX), new, pc.build_batch_info(watermark=200, run_start_ms=2))
|
|
358
|
+
assert _activate(s, old)[0] == 1
|
|
359
|
+
assert _activate(s, new)[0] == 1
|
|
360
|
+
retired = pc.decode_batch_info(r.hget(pc.versions_key(PREFIX), old))
|
|
361
|
+
assert retired["state"] == pc.STATE_RETIRED and retired["rows_expected"] == 5
|
|
362
|
+
assert pc.BATCH_RETIRED_AT in retired
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ---------------------------------------------------------------- SCAN 匹配模式
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def test_scan_patterns_cover_exactly_one_table():
|
|
369
|
+
control, data = pc.table_key_patterns(PREFIX)
|
|
370
|
+
assert control == "{17893778831981982:ie3ton4o6vrh}:*"
|
|
371
|
+
assert data == "17893778831981982:ie3ton4o6vrh:*"
|
|
372
|
+
assert pc.batch_data_pattern(PREFIX, "a3f2c1d8", "mu9k18p8a7") == "17893778831981982:ie3ton4o6vrh:a3f2c1d8:mu9k18p8a7:*"
|
|
373
|
+
assert pc.meta_pattern(PREFIX) == "{17893778831981982:ie3ton4o6vrh}:__meta__:*"
|
|
374
|
+
assert pc.meta_key(PREFIX, "a3f2c1d8").startswith(pc.meta_key_prefix(PREFIX))
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def test_scan_patterns_escape_glob_metacharacters():
|
|
378
|
+
control, data = pc.table_key_patterns("1:a*b?[c]")
|
|
379
|
+
assert control == r"{1:a\*b\?\[c\]}:*"
|
|
380
|
+
assert data == r"1:a\*b\?\[c\]:*"
|