Skip to content

Commit a75480f

Browse files
committed
feat: add wait_for_sync() for reliable sync verification
1 parent 2bfe8fc commit a75480f

4 files changed

Lines changed: 98 additions & 23 deletions

File tree

tests/cases/auth_acl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def acl():
2828
shake = h.Shake(opts)
2929

3030
# wait sync done
31-
p.ASSERT_TRUE_TIMEOUT(lambda: shake.is_consistent(), interval=0.01)
31+
shake.wait_for_sync(timeout=10)
3232
p.log(shake.get_status())
3333

3434
# check data

tests/cases/function.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def filter_db():
2424
src.do("select", db)
2525
src.do("set", "key", "value")
2626

27-
# wait sync done
28-
p.ASSERT_TRUE_TIMEOUT(lambda: shake.is_consistent(), timeout=10, interval=0.01)
27+
# wait sync done (use db=1 because db=0 is filtered)
28+
shake.wait_for_sync(timeout=10, db=1)
2929

3030
dst.do("select", 0)
3131
p.ASSERT_EQ(dst.do("get", "key"), None)
@@ -53,15 +53,21 @@ def split_mset_to_set():
5353
p.log(f"opts: {opts}")
5454
shake = h.Shake(opts)
5555
src.do("mset", "k1", "v1", "k2", "v2", "k3", "v3")
56-
# wait sync done
56+
57+
# wait sync done - check both consistency AND data presence
58+
# is_consistent() can return true before MSET is captured by AOF streaming
59+
def data_synced():
60+
if not shake.is_consistent():
61+
return False
62+
dst.do("select", 1)
63+
return dst.do("get", "k1") == b"v1"
64+
5765
try:
58-
p.ASSERT_TRUE_TIMEOUT(lambda: shake.is_consistent(), timeout=10, interval=0.01)
66+
p.ASSERT_TRUE_TIMEOUT(data_synced, timeout=10, interval=0.01)
5967
except Exception as e:
6068
with open(f"{shake.dir}/data/shake.log") as f:
6169
p.log(f.read())
6270
raise e
63-
dst.do("select", 1)
64-
p.ASSERT_EQ(dst.do("get", "k1"), b"v1")
6571
p.ASSERT_EQ(dst.do("get", "k2"), b"v2")
6672
p.ASSERT_EQ(dst.do("get", "k3"), b"v3")
6773

tests/cases/sync.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import time
2-
31
import pybbt as p
42

53
import helpers as h
@@ -17,8 +15,8 @@ def test(src, dst):
1715
shake = h.Shake(opts)
1816

1917
# wait sync done
20-
try: # HTTPConnectionPool
21-
p.ASSERT_TRUE_TIMEOUT(lambda: shake.is_consistent(), timeout=10, interval=0.01)
18+
try:
19+
shake.wait_for_sync(timeout=10)
2220
except Exception as e:
2321
with open(f"{shake.dir}/data/shake.log") as f:
2422
p.log(f.read())
@@ -28,9 +26,8 @@ def test(src, dst):
2826
inserter.add_data(src, cross_slots_cmd=cross_slots_cmd)
2927

3028
# wait sync done
31-
p.ASSERT_TRUE_TIMEOUT(lambda: shake.is_consistent(), interval=0.01)
29+
shake.wait_for_sync(timeout=10)
3230
p.log(shake.get_status())
33-
time.sleep(5)
3431

3532
# check data
3633
inserter.check_data(src, cross_slots_cmd=cross_slots_cmd)

tests/helpers/shake.py

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import typing
2+
import time
3+
import uuid
24

35
import pybbt
46
import requests
@@ -21,7 +23,9 @@ def create_sync_opts(src: Redis, dst: Redis) -> typing.Dict:
2123
"redis_writer": {
2224
"cluster": dst.is_cluster(),
2325
"address": dst.get_address()
24-
}
26+
},
27+
"__src": src,
28+
"__dst": dst,
2529
}
2630
return d
2731

@@ -35,32 +39,36 @@ def create_scan_opts(src: Redis, dst: Redis) -> typing.Dict:
3539
"redis_writer": {
3640
"cluster": dst.is_cluster(),
3741
"address": dst.get_address()
38-
}
42+
},
43+
"__src": src,
44+
"__dst": dst,
3945
}
4046
return d
4147

4248
@staticmethod
43-
def create_rdb_opts(rdb_path: str, dts: Redis) -> typing.Dict:
49+
def create_rdb_opts(rdb_path: str, dst: Redis) -> typing.Dict:
4450
d = {
4551
"rdb_reader": {"filepath": rdb_path},
4652
"redis_writer": {
47-
"cluster": dts.is_cluster(),
48-
"address": dts.get_address()
49-
}
53+
"cluster": dst.is_cluster(),
54+
"address": dst.get_address()
55+
},
56+
"__dst": dst,
5057
}
5158
return d
5259

5360
@staticmethod
54-
def create_aof_opts(aof_path: str, dts: Redis, timestamp: int = 0) -> typing.Dict:
61+
def create_aof_opts(aof_path: str, dst: Redis, timestamp: int = 0) -> typing.Dict:
5562
d = {
5663
"aof_reader": {"filepath": aof_path, "timestamp": timestamp},
5764
"redis_writer": {
58-
"cluster": dts.is_cluster(),
59-
"address": dts.get_address()
65+
"cluster": dst.is_cluster(),
66+
"address": dst.get_address()
6067
},
6168
"advanced": {
6269
"log_level": "debug"
63-
}
70+
},
71+
"__dst": dst,
6472
}
6573
return d
6674

@@ -70,6 +78,11 @@ def __init__(self, opts: typing.Dict):
7078
self.case_ctx = pybbt.get_case_context()
7179
self.status_port = get_free_port()
7280
self.status_url = f"http://localhost:{self.status_port}"
81+
82+
# 提取并保存 src/dst 引用
83+
self.src = opts.pop("__src", None)
84+
self.dst = opts.pop("__dst", None)
85+
7386
opts["advanced"] = {"status_port": self.status_port, "log_level": "debug"}
7487

7588
self.dir = f"{self.case_ctx.dir}/shake{self.status_port}"
@@ -82,6 +95,9 @@ def __init__(self, opts: typing.Dict):
8295

8396
@staticmethod
8497
def run_once(opts: typing.Dict):
98+
# 移除内部字段
99+
opts.pop("__src", None)
100+
opts.pop("__dst", None)
85101
status_port = get_free_port()
86102
run_dir = f"{pybbt.get_case_context().dir}/shake{status_port}"
87103
create_empty_dir(run_dir)
@@ -107,3 +123,59 @@ def _wait_start(self, timeout=5):
107123

108124
def is_consistent(self):
109125
return self.get_status()["consistent"]
126+
127+
def wait_for_sync(self, timeout: float = 5, db: int = 0):
128+
"""
129+
Wait for data to be fully synced from src to dst.
130+
131+
1. Wait for is_consistent() - RDB sync done
132+
2. Check dbsize matches
133+
3. Write marker key to src
134+
4. Wait for marker to appear in dst
135+
5. Delete marker key
136+
6. Wait for consistent again
137+
"""
138+
if self.src is None or self.dst is None:
139+
raise ValueError("src and dst not available, use ShakeOpts.create_sync_opts() or create_scan_opts()")
140+
141+
timer = Timer()
142+
143+
# 1. Wait for is_consistent()
144+
while not self.is_consistent():
145+
if timer.elapsed() > timeout:
146+
raise TimeoutError(f"is_consistent() not reached within {timeout}s")
147+
time.sleep(0.01)
148+
149+
# 2. Check dbsize (cluster doesn't support SELECT, use db 0)
150+
if not self.src.is_cluster():
151+
self.src.do("select", db)
152+
if not self.dst.is_cluster():
153+
self.dst.do("select", db)
154+
src_dbsize = self.src.dbsize()
155+
while self.dst.dbsize() != src_dbsize:
156+
if timer.elapsed() > timeout:
157+
raise TimeoutError(f"dbsize mismatch within {timeout}s: src={src_dbsize}, dst={self.dst.dbsize()}")
158+
time.sleep(0.01)
159+
160+
# 3. Write marker key
161+
marker_key = f"__shake_sync_marker_{uuid.uuid4()}"
162+
marker_value = str(time.time())
163+
self.src.do("set", marker_key, marker_value)
164+
165+
# 4. Wait for marker in dst
166+
while True:
167+
result = self.dst.do("get", marker_key)
168+
if result == marker_value.encode():
169+
break
170+
if timer.elapsed() > timeout:
171+
raise TimeoutError(f"marker key not synced within {timeout}s")
172+
time.sleep(0.01)
173+
174+
# 5. Delete marker key
175+
self.src.do("del", marker_key)
176+
177+
# 6. Wait for consistent again
178+
while not self.is_consistent():
179+
if timer.elapsed() > timeout:
180+
raise TimeoutError(f"final is_consistent() not reached within {timeout}s")
181+
time.sleep(0.01)

0 commit comments

Comments
 (0)