Skip to content

Commit 742a81a

Browse files
authored
filelock for test- plots and blocks (#20193)
* introduce file lock for creating the test-plots in tests. This avoids having every process in pytest-xdist create the same plots, clobbering each other * add lockfile to the test fixtures that generate the test blockchains. These are quite expensive to generate, we don't want every pytest-xdist process to have to do it * improve error message when the plot manager has an unexpected number of plots, in the simulator * review comments: use FileLock instead of custom made
1 parent 657b637 commit 742a81a

File tree

3 files changed

+112
-66
lines changed

3 files changed

+112
-66
lines changed

chia/_tests/conftest.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,11 @@ def blockchain_constants(consensus_mode: ConsensusMode) -> ConsensusConstants:
230230

231231

232232
@pytest.fixture(scope="session", name="bt")
233-
async def block_tools_fixture(get_keychain, blockchain_constants, anyio_backend) -> BlockTools:
233+
async def block_tools_fixture(get_keychain, blockchain_constants, anyio_backend, testrun_uid: str) -> BlockTools:
234234
# Note that this causes a lot of CPU and disk traffic - disk, DB, ports, process creation ...
235-
shared_block_tools = await create_block_tools_async(constants=blockchain_constants, keychain=get_keychain)
235+
shared_block_tools = await create_block_tools_async(
236+
constants=blockchain_constants, keychain=get_keychain, testrun_uid=testrun_uid
237+
)
236238
return shared_block_tools
237239

238240

@@ -950,13 +952,17 @@ def get_temp_keyring():
950952

951953

952954
@pytest.fixture(scope="function")
953-
async def get_b_tools_1(get_temp_keyring):
954-
return await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring)
955+
async def get_b_tools_1(get_temp_keyring, testrun_uid: str):
956+
return await create_block_tools_async(
957+
constants=test_constants_modified, keychain=get_temp_keyring, testrun_uid=testrun_uid
958+
)
955959

956960

957961
@pytest.fixture(scope="function")
958-
async def get_b_tools(get_temp_keyring):
959-
local_b_tools = await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring)
962+
async def get_b_tools(get_temp_keyring, testrun_uid):
963+
local_b_tools = await create_block_tools_async(
964+
constants=test_constants_modified, keychain=get_temp_keyring, testrun_uid=testrun_uid
965+
)
960966
new_config = local_b_tools._config
961967
local_b_tools.change_config(new_config)
962968
return local_b_tools
@@ -1258,7 +1264,9 @@ def populated_temp_file_keyring_fixture() -> Iterator[TempKeyring]:
12581264

12591265
@pytest.fixture(scope="function")
12601266
async def farmer_harvester_2_simulators_zero_bits_plot_filter(
1261-
tmp_path: Path, get_temp_keyring: Keychain
1267+
tmp_path: Path,
1268+
get_temp_keyring: Keychain,
1269+
testrun_uid,
12621270
) -> AsyncIterator[
12631271
tuple[
12641272
FarmerService,
@@ -1277,6 +1285,7 @@ async def farmer_harvester_2_simulators_zero_bits_plot_filter(
12771285
bt = await create_block_tools_async(
12781286
zero_bit_plot_filter_consts,
12791287
keychain=get_temp_keyring,
1288+
testrun_uid=testrun_uid,
12801289
)
12811290

12821291
config_overrides: dict[str, int] = {"full_node.max_sync_wait": 0}
@@ -1289,6 +1298,7 @@ async def farmer_harvester_2_simulators_zero_bits_plot_filter(
12891298
num_pool_plots=0,
12901299
num_non_keychain_plots=0,
12911300
config_overrides=config_overrides,
1301+
testrun_uid=testrun_uid,
12921302
)
12931303
for _ in range(2)
12941304
]

chia/_tests/util/blockchain.py

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from chia_rs import ConsensusConstants, FullBlock
1111
from chia_rs.sized_ints import uint64
12+
from filelock import FileLock
1213

1314
from chia.consensus.block_height_map import BlockHeightMap
1415
from chia.consensus.blockchain import Blockchain
@@ -59,47 +60,49 @@ def persistent_blocks(
5960
block_list_input = []
6061
block_path_dir = DEFAULT_ROOT_PATH.parent.joinpath("blocks")
6162
file_path = block_path_dir.joinpath(db_name)
63+
lock_file_path = block_path_dir / (db_name + ".lockfile")
6264

6365
ci = os.environ.get("CI")
6466
if ci is not None and not file_path.exists():
6567
raise Exception(f"Running in CI and expected path not found: {file_path!r}")
6668

6769
block_path_dir.mkdir(parents=True, exist_ok=True)
6870

69-
if file_path.exists():
70-
print(f"File found at: {file_path}")
71-
try:
72-
bytes_list = file_path.read_bytes()
73-
# TODO: use explicit serialization instead of pickle
74-
block_bytes_list: list[bytes] = pickle.loads(bytes_list) # noqa: S301
75-
blocks: list[FullBlock] = []
76-
for block_bytes in block_bytes_list:
77-
blocks.append(FullBlock.from_bytes_unchecked(block_bytes))
78-
if len(blocks) == num_of_blocks + len(block_list_input):
79-
print(f"\n loaded {file_path} with {len(blocks)} blocks")
71+
with FileLock(lock_file_path):
72+
if file_path.exists():
73+
print(f"File found at: {file_path}")
74+
try:
75+
bytes_list = file_path.read_bytes()
76+
# TODO: use explicit serialization instead of pickle
77+
block_bytes_list: list[bytes] = pickle.loads(bytes_list) # noqa: S301
78+
blocks: list[FullBlock] = []
79+
for block_bytes in block_bytes_list:
80+
blocks.append(FullBlock.from_bytes_unchecked(block_bytes))
81+
if len(blocks) == num_of_blocks + len(block_list_input):
82+
print(f"\n loaded {file_path} with {len(blocks)} blocks")
8083

81-
return blocks
82-
except EOFError:
83-
print("\n error reading db file")
84-
else:
85-
print(f"File not found at: {file_path}")
84+
return blocks
85+
except EOFError:
86+
print("\n error reading db file")
87+
else:
88+
print(f"File not found at: {file_path}")
8689

87-
print("Creating a new test db")
88-
return new_test_db(
89-
file_path,
90-
num_of_blocks,
91-
seed,
92-
empty_sub_slots,
93-
bt,
94-
block_list_input,
95-
time_per_block,
96-
normalized_to_identity_cc_eos=normalized_to_identity_cc_eos,
97-
normalized_to_identity_icc_eos=normalized_to_identity_icc_eos,
98-
normalized_to_identity_cc_sp=normalized_to_identity_cc_sp,
99-
normalized_to_identity_cc_ip=normalized_to_identity_cc_ip,
100-
dummy_block_references=dummy_block_references,
101-
include_transactions=include_transactions,
102-
)
90+
print("Creating a new test db")
91+
return new_test_db(
92+
file_path,
93+
num_of_blocks,
94+
seed,
95+
empty_sub_slots,
96+
bt,
97+
block_list_input,
98+
time_per_block,
99+
normalized_to_identity_cc_eos=normalized_to_identity_cc_eos,
100+
normalized_to_identity_icc_eos=normalized_to_identity_icc_eos,
101+
normalized_to_identity_cc_sp=normalized_to_identity_cc_sp,
102+
normalized_to_identity_cc_ip=normalized_to_identity_cc_ip,
103+
dummy_block_references=dummy_block_references,
104+
include_transactions=include_transactions,
105+
)
103106

104107

105108
def new_test_db(

chia/simulator/block_tools.py

Lines changed: 60 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
)
4242
from chia_rs.sized_bytes import bytes32
4343
from chia_rs.sized_ints import uint8, uint16, uint32, uint64, uint128
44+
from filelock import FileLock
4445

4546
from chia.consensus.block_creation import create_unfinished_block, unfinished_block_to_full_block
4647
from chia.consensus.block_record import BlockRecordProtocol
@@ -338,6 +339,29 @@ def test_callback(event: PlotRefreshEvents, update_result: PlotRefreshResult) ->
338339
assert self.total_result.processed == update_result.processed
339340
assert self.total_result.duration == update_result.duration
340341
assert update_result.remaining == 0
342+
343+
expected_plots: set[str] = set()
344+
found_plots: set[str] = set()
345+
if len(self.plot_manager.plots) != len(self.expected_plots): # pragma: no cover
346+
for pid, filename in self.expected_plots.items():
347+
expected_plots.add(filename.name)
348+
for filename, _ in self.plot_manager.plots.items():
349+
found_plots.add(filename.name)
350+
print(f"directory: {self.plot_dir}")
351+
print(f"expected: {len(expected_plots)}")
352+
for f in expected_plots:
353+
print(f)
354+
print(f"plot manager: {len(found_plots)}")
355+
for f in found_plots:
356+
print(f)
357+
diff = found_plots.difference(expected_plots)
358+
print(f"found unexpected: {len(diff)}")
359+
for f in diff:
360+
print(f)
361+
diff = expected_plots.difference(found_plots)
362+
print(f"not found: {len(diff)}")
363+
for f in diff:
364+
print(f)
341365
assert len(self.plot_manager.plots) == len(self.expected_plots)
342366

343367
self.plot_manager: PlotManager = PlotManager(
@@ -500,34 +524,41 @@ async def setup_plots(
500524
num_non_keychain_plots: int = 3,
501525
plot_size: int = 20,
502526
bitfield: bool = True,
527+
testrun_uid: Optional[str] = None,
503528
) -> bool:
504-
self.add_plot_directory(self.plot_dir)
505-
assert self.created_plots == 0
506-
existing_plots: bool = True
507-
# OG Plots
508-
for i in range(num_og_plots):
509-
plot = await self.new_plot(plot_size=plot_size, bitfield=bitfield)
510-
if plot.new_plot:
511-
existing_plots = False
512-
# Pool Plots
513-
for i in range(num_pool_plots):
514-
plot = await self.new_plot(self.pool_ph, plot_size=plot_size, bitfield=bitfield)
515-
if plot.new_plot:
516-
existing_plots = False
517-
# Some plots with keys that are not in the keychain
518-
for i in range(num_non_keychain_plots):
519-
plot = await self.new_plot(
520-
path=self.plot_dir / "not_in_keychain",
521-
plot_keys=PlotKeys(G1Element(), G1Element(), None),
522-
exclude_plots=True,
523-
plot_size=plot_size,
524-
bitfield=bitfield,
525-
)
526-
if plot.new_plot:
527-
existing_plots = False
528-
await self.refresh_plots()
529-
assert len(self.plot_manager.plots) == len(self.expected_plots)
530-
return existing_plots
529+
if testrun_uid is None:
530+
lock_file_name = self.plot_dir / ".lockfile"
531+
else:
532+
lock_file_name = self.plot_dir / (testrun_uid + ".lockfile")
533+
534+
with FileLock(lock_file_name):
535+
self.add_plot_directory(self.plot_dir)
536+
assert self.created_plots == 0
537+
existing_plots: bool = True
538+
# OG Plots
539+
for i in range(num_og_plots):
540+
plot = await self.new_plot(plot_size=plot_size, bitfield=bitfield)
541+
if plot.new_plot:
542+
existing_plots = False
543+
# Pool Plots
544+
for i in range(num_pool_plots):
545+
plot = await self.new_plot(self.pool_ph, plot_size=plot_size, bitfield=bitfield)
546+
if plot.new_plot:
547+
existing_plots = False
548+
# Some plots with keys that are not in the keychain
549+
for i in range(num_non_keychain_plots):
550+
plot = await self.new_plot(
551+
path=self.plot_dir / "not_in_keychain",
552+
plot_keys=PlotKeys(G1Element(), G1Element(), None),
553+
exclude_plots=True,
554+
plot_size=plot_size,
555+
bitfield=bitfield,
556+
)
557+
if plot.new_plot:
558+
existing_plots = False
559+
await self.refresh_plots()
560+
assert len(self.plot_manager.plots) == len(self.expected_plots)
561+
return existing_plots
531562

532563
async def new_plot(
533564
self,
@@ -2080,6 +2111,7 @@ async def create_block_tools_async(
20802111
num_og_plots: int = 15,
20812112
num_pool_plots: int = 5,
20822113
num_non_keychain_plots: int = 3,
2114+
testrun_uid: Optional[str] = None,
20832115
) -> BlockTools:
20842116
global create_block_tools_async_count
20852117
create_block_tools_async_count += 1
@@ -2090,6 +2122,7 @@ async def create_block_tools_async(
20902122
num_og_plots=num_og_plots,
20912123
num_pool_plots=num_pool_plots,
20922124
num_non_keychain_plots=num_non_keychain_plots,
2125+
testrun_uid=testrun_uid,
20932126
)
20942127

20952128
return bt

0 commit comments

Comments
 (0)