Skip to content

Commit 7c5920a

Browse files
frank-emrichmeta-codesync[bot]
authored andcommitted
avoid invariant violations due to previously crashed processes
Summary: Prior to this diff, the following sequence of events caused an invariant violation: 1. `hh_server` is not running. 2. Some other application A crashes while asserting a state X, leaving a spurious X.notify file 3. `hh_server` is started, and correctly considers state X as not being asserted 4. Application A is started again and asserts X again. This causes it to delete the X.notify file first, which triggers a `StateLeave(X)` event followed by a `StateEnter(X)`. 5. `hh_server` receives `StateLeave(X)` violating the invariant that we must note leave a state that's not asserted. Note that this can only happen if A crashed *before* hh was started. We already handle the case correctly when an application asserting a state crashes while `hh_server` is running. This diff fixes this issue: As of D91942985, `which_states_asserted` cleans up all spurious X.notify files for us during server initialization. The only thing this diff does is ensuring that we ask Eden for the initial journal position during initialization *after* calling `which_states_asserted`, so that we skip over the `StateLeave` events due to the cleanup. Note that this also means that our code in `Edenfs_watcher` continues not to interact with .notify files. As a drive-by change, I'm also switching us from using `get_asserted_states` to `which_states_asserted` for getting the initially asserted states during server start. The only difference between the two is that the latter is supposed to be the public API and does the filtering of state names for us. All the changes in this diff only affect behaviour if `edenfs_file_watcher_state_tracking` is enabled, which is disabled for everyone at the moment. Reviewed By: Alramech Differential Revision: D92404246 fbshipit-source-id: 88f21edbc46737ebc173e0ce7fec8d1c4b4fe19b
1 parent 86b9702 commit 7c5920a

1 file changed

Lines changed: 73 additions & 11 deletions

File tree

hphp/hack/test/integration/test_edenfs_file_watcher.py

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -454,16 +454,17 @@ def createIgnoredFiles(cls) -> None:
454454

455455
def assertStateForSeconds(
456456
self, state_name: str, duration_secs: int
457-
) -> Tuple[Callable[[], None], Callable[[], bool]]:
457+
) -> Tuple[Callable[[], None], Callable[[], bool], Callable[[], None]]:
458458
"""Asserts the given state for the given number of seconds with Eden.
459459
460460
This function returns as soon as the state is asserted.
461461
The state then remains asserted in the background.
462462
463-
Returns to thunks:
463+
Returns three thunks:
464464
- Calling the first one waits until the state is de-asserted
465465
- Calling the second returns a boolean indicating whether the state is
466466
still asserted.
467+
- Calling the third kills the asserter process with SIGKILL.
467468
"""
468469

469470
eden_state_asserter_path = os.getenv("HH_EDEN_TEST_STATE_ASSERTER", None)
@@ -496,7 +497,10 @@ def wait_and_check() -> None:
496497
def is_still_asserted() -> bool:
497498
return proc.poll() is None
498499

499-
return (wait_and_check, is_still_asserted)
500+
def kill_asserter() -> None:
501+
proc.kill()
502+
503+
return (wait_and_check, is_still_asserted, kill_asserter)
500504

501505

502506
class EdenfsWatcherTests(common_tests.CommonTests):
@@ -1155,7 +1159,7 @@ def test_deferral1(self) -> None:
11551159

11561160
self.test_driver.createNonHackFile("file1.php")
11571161

1158-
wait_for0, is_asserted0 = self.test_driver.assertStateForSeconds(state0, 10)
1162+
wait_for0, is_asserted0, _ = self.test_driver.assertStateForSeconds(state0, 10)
11591163

11601164
self.test_driver.createNonHackFile("file2.php")
11611165

@@ -1200,7 +1204,7 @@ def run_instant_deassertion_test(
12001204
# 30 iterations keeps this test at around 1.5 minutes
12011205
for iterations in range(1, 30):
12021206
for _ in range(iterations):
1203-
wait_thunk, _ = self.test_driver.assertStateForSeconds(state, 0)
1207+
wait_thunk, _, _ = self.test_driver.assertStateForSeconds(state, 0)
12041208
if wait_for_deassert:
12051209
wait_thunk()
12061210

@@ -1239,7 +1243,7 @@ def test_deferral6(self) -> None:
12391243

12401244
self.test_driver.createNonHackFile("file0.php")
12411245

1242-
wait_for0, _ = self.test_driver.assertStateForSeconds(state0, 20)
1246+
wait_for0, _, _ = self.test_driver.assertStateForSeconds(state0, 20)
12431247

12441248
self.test_driver.createNonHackFile("file1.php")
12451249

@@ -1250,7 +1254,7 @@ def test_deferral6(self) -> None:
12501254
)
12511255

12521256
# will be deasserted while state0 is still asserted
1253-
wait_for1, _ = self.test_driver.assertStateForSeconds(state1, 10)
1257+
wait_for1, _, _ = self.test_driver.assertStateForSeconds(state1, 10)
12541258

12551259
self.test_driver.createNonHackFile("file2.php")
12561260

@@ -1304,7 +1308,7 @@ def test_deferral7(self) -> None:
13041308

13051309
self.test_driver.createNonHackFile("file0.php")
13061310

1307-
wait_for0, _ = self.test_driver.assertStateForSeconds(state0, 20)
1311+
wait_for0, _, _ = self.test_driver.assertStateForSeconds(state0, 20)
13081312

13091313
self.test_driver.createNonHackFile("file1.php")
13101314

@@ -1316,7 +1320,7 @@ def test_deferral7(self) -> None:
13161320
)
13171321

13181322
# will be deasserted while state0 is still asserted
1319-
wait_for1, _ = self.test_driver.assertStateForSeconds(state1, 10)
1323+
wait_for1, _, _ = self.test_driver.assertStateForSeconds(state1, 10)
13201324

13211325
self.test_driver.createNonHackFile("file2.php")
13221326

@@ -1350,7 +1354,7 @@ def test_deferral8(self) -> None:
13501354
)
13511355

13521356
state = TEST_STATE_0
1353-
_, is_asserted = self.test_driver.assertStateForSeconds(state, 20)
1357+
_, is_asserted, _ = self.test_driver.assertStateForSeconds(state, 20)
13541358

13551359
# Note that we asserted before server startup
13561360
self.test_driver.start_hh_server()
@@ -1377,7 +1381,7 @@ def test_deferral9(self) -> None:
13771381
self.test_driver.start_hh_server()
13781382

13791383
# Assert a state that is NOT in the tracked_states list
1380-
_, is_still_asserted = self.test_driver.assertStateForSeconds(
1384+
_, is_still_asserted, _ = self.test_driver.assertStateForSeconds(
13811385
"untracked-state", 30
13821386
)
13831387

@@ -1394,6 +1398,64 @@ def test_deferral9(self) -> None:
13941398
# ... and we should also not have waited until deferral.
13951399
self.assertTrue(is_still_asserted())
13961400

1401+
def test_deferral10(self) -> None:
1402+
"""Test asserting application crashing while hh is running"""
1403+
config = self.test_driver.getConfig()
1404+
config.state_tracking = True
1405+
config.write_hhconf(
1406+
self.test_driver.watchman_instance.getUnixSockPath(),
1407+
self.test_driver.repo_dir,
1408+
)
1409+
1410+
self.test_driver.start_hh_server()
1411+
self.test_driver.check_cmd(["No errors!"])
1412+
1413+
for i, kill_after_secs in enumerate([0, 0.1, 0.5, 1]):
1414+
# Assert state for a long duration (we'll kill it before it finishes)
1415+
_, _, kill_asserter = self.test_driver.assertStateForSeconds(
1416+
TEST_STATE_0, 60
1417+
)
1418+
1419+
time.sleep(kill_after_secs)
1420+
1421+
# SIGKILL the asserter while it's asserting TEST_STATE_0
1422+
kill_asserter()
1423+
1424+
# The asserter is dead, we should not be deferring anymore
1425+
file = f"file{i}.php"
1426+
self.test_driver.createNonHackFile(file)
1427+
self.test_driver.check_cmd(
1428+
[
1429+
f"ERROR: {{root}}{file}:1:1,1: A .php file must begin with `<?hh`. (Parsing[1002])",
1430+
]
1431+
)
1432+
os.remove(os.path.join(self.test_driver.repo_dir, file))
1433+
1434+
def test_deferral11(self) -> None:
1435+
"""Regression test for when an application asserting a state crashed before starting the server."""
1436+
config = self.test_driver.getConfig()
1437+
config.state_tracking = True
1438+
config.write_hhconf(
1439+
self.test_driver.watchman_instance.getUnixSockPath(),
1440+
self.test_driver.repo_dir,
1441+
)
1442+
1443+
# Assert state for a long duration (we'll kill it before it finishes)
1444+
_, _, kill_asserter = self.test_driver.assertStateForSeconds(TEST_STATE_0, 60)
1445+
1446+
# SIGKILL the asserter while it's asserting TEST_STATE_0
1447+
kill_asserter()
1448+
1449+
self.test_driver.start_hh_server()
1450+
self.test_driver.check_cmd(["No errors!"])
1451+
1452+
# Let's assert the state again
1453+
wait_for_deassert, _, _ = self.test_driver.assertStateForSeconds(
1454+
TEST_STATE_0, 1
1455+
)
1456+
wait_for_deassert()
1457+
self.test_driver.check_cmd(["No errors!"])
1458+
13971459

13981460
class EdenfsWatcherNonMountPointRepoTests(EdenfsWatcherTests):
13991461
"""Runs the same tests as EdenfsWatcherTests, but with a testing repo that's not the mount point of the Eden mount.

0 commit comments

Comments
 (0)