Skip to content

Commit acd5be7

Browse files
Merge pull request #478 from HashemKhalifa/codex/fix-worktree-store-resolution
fix: stabilize worktree storage and daemon lifecycle
2 parents 5a381bc + caeb4f7 commit acd5be7

12 files changed

Lines changed: 2270 additions & 116 deletions

File tree

src/daemon.rs

Lines changed: 376 additions & 22 deletions
Large diffs are not rendered by default.

src/daemon/branch_admin.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ impl StoreAdministration {
120120
ensure_no_external_branch_store_holders(database_paths)
121121
}
122122

123+
/// Reports whether writer administration is already held. Work that would
124+
/// queue behind an unrelated writer can be delayed for that writer's whole
125+
/// operation, which callers may want to answer with a retry hint instead.
126+
pub(super) fn writer_is_busy(&self) -> bool {
127+
self.gate.try_lock().is_err()
128+
}
129+
123130
/// Acquires writer administration before constructing the supplied future
124131
/// and holds it until that future completes.
125132
pub(super) async fn with_writer<Operation, OperationFuture, Output>(

src/daemon/scheduler.rs

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::errors::{Result, TraceDecayError};
99

1010
use super::{
1111
DAEMON_TASK_ABORT_DEADLINE, DaemonEngine, DaemonHandshake, ProjectServerKey, log_daemon_event,
12-
open_existing_project_with_options,
12+
open_existing_project_with_options, spawn_lifecycle_automation_scheduler_activation,
1313
};
1414

1515
pub(super) fn scheduler_task_log_fields(
@@ -158,12 +158,73 @@ impl DaemonEngine {
158158
key: ProjectServerKey,
159159
project_path: PathBuf,
160160
handshake: DaemonHandshake,
161+
cg: Arc<crate::tracedecay::TraceDecay>,
161162
) {
163+
if !self.lifecycle.accepting() {
164+
return;
165+
}
166+
{
167+
let schedulers = self
168+
.store_administration
169+
.automation_schedulers()
170+
.lock()
171+
.await;
172+
if schedulers.contains_key(&key) {
173+
return;
174+
}
175+
}
176+
177+
// Discover configuration from the project server that is already
178+
// registered for this owner. This keeps writable project opening and
179+
// identity repair under the project-server coordination path while
180+
// avoiding the daemon-wide writer gate for read-only discovery.
181+
let configured = match async {
182+
let config =
183+
effective_automation_config_for_project(&cg, &handshake.client_identity).await?;
184+
automation_scheduler_has_work(&cg, &config).await
185+
}
186+
.await
187+
{
188+
Ok(configured) => configured,
189+
Err(e) => {
190+
log_daemon_event(
191+
"scheduler_config",
192+
&[
193+
("project", project_path.display().to_string()),
194+
("outcome", "error".to_string()),
195+
("error", e.to_string()),
196+
],
197+
);
198+
false
199+
}
200+
};
201+
if !configured {
202+
log_daemon_event(
203+
"scheduler_config",
204+
&[
205+
("project", project_path.display().to_string()),
206+
("outcome", "skipped".to_string()),
207+
("reason", "not_configured".to_string()),
208+
],
209+
);
210+
return;
211+
}
212+
162213
self.store_administration
163214
.with_writer(|| async move {
164215
if !self.lifecycle.accepting() {
165216
return;
166217
}
218+
let owner_is_current = self
219+
.store_administration
220+
.project_servers()
221+
.lock()
222+
.await
223+
.get(&key)
224+
.is_some();
225+
if !owner_is_current {
226+
return;
227+
}
167228
{
168229
let schedulers = self
169230
.store_administration
@@ -174,38 +235,6 @@ impl DaemonEngine {
174235
return;
175236
}
176237
}
177-
178-
let configured = match Box::pin(automation_scheduler_has_work_for_project(
179-
&project_path,
180-
&handshake,
181-
))
182-
.await
183-
{
184-
Ok(configured) => configured,
185-
Err(e) => {
186-
log_daemon_event(
187-
"scheduler_config",
188-
&[
189-
("project", project_path.display().to_string()),
190-
("outcome", "error".to_string()),
191-
("error", e.to_string()),
192-
],
193-
);
194-
false
195-
}
196-
};
197-
if !configured {
198-
log_daemon_event(
199-
"scheduler_config",
200-
&[
201-
("project", project_path.display().to_string()),
202-
("outcome", "skipped".to_string()),
203-
("reason", "not_configured".to_string()),
204-
],
205-
);
206-
return;
207-
}
208-
209238
self.start_automation_scheduler(key, project_path, handshake)
210239
.await;
211240
})
@@ -224,10 +253,24 @@ impl DaemonEngine {
224253
let current_key = Arc::clone(&current_key);
225254
let project_path = project_path.clone();
226255
let handshake = handshake.clone();
227-
tokio::spawn(async move {
256+
let lifecycle = engine.lifecycle.clone();
257+
spawn_lifecycle_automation_scheduler_activation(lifecycle, async move {
228258
let key = current_key.lock().await.clone();
259+
let server = {
260+
engine
261+
.store_administration
262+
.project_servers()
263+
.lock()
264+
.await
265+
.get(&key)
266+
.cloned()
267+
};
268+
let Some(server) = server else {
269+
return;
270+
};
271+
let cg = server.cg().await;
229272
engine
230-
.ensure_automation_scheduler(key.clone(), project_path, handshake)
273+
.ensure_automation_scheduler(key.clone(), project_path, handshake, cg)
231274
.await;
232275
if let Some(handle) = engine
233276
.store_administration

src/daemon/service.rs

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ use super::SOCKET_ENV;
1212

1313
const LAUNCHD_LABEL: &str = "com.tracedecay.daemon";
1414
const LAUNCHD_PLIST_NAME: &str = "com.tracedecay.daemon.plist";
15+
// Cached project owners retain SQLite families and coordination locks. The
16+
// platform default of 256 descriptors is too small for multi-worktree use.
17+
const DAEMON_OPEN_FILE_LIMIT: u32 = 8_192;
1518

1619
#[derive(Debug, Clone, PartialEq, Eq)]
1720
pub struct DaemonServiceSpec {
@@ -54,12 +57,14 @@ impl DaemonServiceSpec {
5457
ExecStart={} daemon run --socket {}\n\
5558
Restart=on-failure\n\
5659
RestartSec=2\n\
60+
LimitNOFILE={}\n\
5761
\n\
5862
[Install]\n\
5963
WantedBy=default.target\n",
6064
systemd_escape_env_value(&service_path),
6165
self.tracedecay_bin.display(),
62-
self.socket_path.display()
66+
self.socket_path.display(),
67+
DAEMON_OPEN_FILE_LIMIT,
6368
)
6469
}
6570

@@ -136,6 +141,12 @@ impl DaemonServiceSpec {
136141
\n\
137142
<key>ThrottleInterval</key>\n\
138143
<integer>2</integer>\n\
144+
\n\
145+
<key>SoftResourceLimits</key>\n\
146+
<dict>\n\
147+
<key>NumberOfFiles</key>\n\
148+
<integer>{open_file_limit}</integer>\n\
149+
</dict>\n\
139150
\n\
140151
<key>StandardOutPath</key>\n\
141152
<string>{stdout}</string>\n\
@@ -147,6 +158,7 @@ impl DaemonServiceSpec {
147158
label = plist_xml_escape(LAUNCHD_LABEL),
148159
bin = plist_xml_escape(&self.tracedecay_bin.display().to_string()),
149160
socket = plist_xml_escape(&self.socket_path.display().to_string()),
161+
open_file_limit = DAEMON_OPEN_FILE_LIMIT,
150162
stdout = plist_xml_escape(&data_dir.join("daemon.out.log").display().to_string()),
151163
stderr = plist_xml_escape(&data_dir.join("daemon.err.log").display().to_string()),
152164
))
@@ -373,8 +385,41 @@ fn refresh_installed_service_with_state(
373385
refresh_service_with_runner(&runner, &refreshed_spec, previous_state).map(Some)
374386
}
375387

388+
/// Stops a managed daemon before `daemon restart` acquires exclusive lifecycle
389+
/// ownership. The daemon itself holds a shared lease while it is running.
390+
#[doc(hidden)]
391+
pub fn quiesce_installed_service_for_restart() -> Result<DaemonServiceState> {
392+
quiesce_installed_service()
393+
}
394+
376395
#[doc(hidden)]
377396
pub fn quiesce_installed_service_under_lease() -> Result<DaemonServiceState> {
397+
quiesce_installed_service()
398+
}
399+
400+
/// Restores an installed service that was running before restart quiesced it.
401+
/// This deliberately reuses the existing unit instead of rewriting it because
402+
/// the caller invokes it only after exclusive lifecycle acquisition failed.
403+
#[doc(hidden)]
404+
pub fn restore_quiesced_installed_service(previous_state: DaemonServiceState) -> Result<()> {
405+
if !cfg!(any(target_os = "linux", target_os = "macos")) || !previous_state.is_running() {
406+
return Ok(());
407+
}
408+
let service_path = service_unit_path()?;
409+
if !service_path.exists() {
410+
return Err(TraceDecayError::Config {
411+
message: format!(
412+
"cannot restore missing TraceDecay daemon service '{}'",
413+
service_path.display()
414+
),
415+
});
416+
}
417+
let unit = read_service_unit(&service_path)?;
418+
let socket_path = socket_path_from_unit_text(&unit).unwrap_or(default_socket_path()?);
419+
ServiceRunner::current()?.restore_after_quiesce(&service_path, &socket_path, previous_state)
420+
}
421+
422+
fn quiesce_installed_service() -> Result<DaemonServiceState> {
378423
if !cfg!(any(target_os = "linux", target_os = "macos")) {
379424
return Ok(DaemonServiceState::Missing);
380425
}
@@ -758,6 +803,27 @@ impl ServiceRunner {
758803
}
759804
}
760805

806+
fn restore_after_quiesce(
807+
&self,
808+
service_path: &Path,
809+
socket_path: &Path,
810+
previous_state: DaemonServiceState,
811+
) -> Result<()> {
812+
if !previous_state.is_running() {
813+
return Ok(());
814+
}
815+
match self {
816+
Self::Systemd => run_systemctl(&["start", super::SERVICE_NAME]),
817+
Self::Launchd => {
818+
launchd_refresh(service_path, socket_path)?;
819+
if !previous_state.is_enabled() {
820+
run_launchctl(&["disable", &launchd_service_target()?])?;
821+
}
822+
Ok(())
823+
}
824+
}
825+
}
826+
761827
fn after_uninstall(&self, stop: bool) {
762828
match self {
763829
Self::Systemd => {
@@ -1224,6 +1290,7 @@ mod tests {
12241290
));
12251291
assert!(unit.contains("Environment=\"PATH="));
12261292
assert!(unit.contains("Restart=on-failure"));
1293+
assert!(unit.contains("LimitNOFILE=8192"));
12271294
}
12281295

12291296
// The launchd render tests use Unix-style absolute binary paths, which
@@ -1265,6 +1332,9 @@ mod tests {
12651332
assert!(plist.contains("<key>TRACEDECAY_DATA_DIR</key>"));
12661333
assert!(plist.contains("<key>RunAtLoad</key>"));
12671334
assert!(plist.contains("<key>KeepAlive</key>"));
1335+
assert!(plist.contains("<key>SoftResourceLimits</key>"));
1336+
assert!(plist.contains("<key>NumberOfFiles</key>"));
1337+
assert!(plist.contains("<integer>8192</integer>"));
12681338
}
12691339

12701340
#[cfg(unix)]
@@ -1619,6 +1689,55 @@ mod tests {
16191689
);
16201690
}
16211691

1692+
#[cfg(target_os = "linux")]
1693+
#[test]
1694+
fn restore_quiesced_service_starts_existing_unit_without_rewriting_it() {
1695+
let _env_lock = lock_user_data_dir_test_env();
1696+
let dir = TempDir::new().expect("temp dir");
1697+
let config_home = dir.path().join("config");
1698+
let fake_bin = dir.path().join("bin");
1699+
let home = dir.path().join("home");
1700+
std::fs::create_dir_all(&fake_bin).expect("fake bin dir");
1701+
std::fs::create_dir_all(&home).expect("home dir");
1702+
1703+
let systemctl = fake_bin.join("systemctl");
1704+
let log = dir.path().join("systemctl.log");
1705+
std::fs::write(
1706+
&systemctl,
1707+
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$TRACEDECAY_SYSTEMCTL_LOG\"\nexit 0\n",
1708+
)
1709+
.expect("fake systemctl");
1710+
std::fs::set_permissions(&systemctl, std::fs::Permissions::from_mode(0o755))
1711+
.expect("systemctl permissions");
1712+
1713+
let _config_guard = EnvVarGuard::set("XDG_CONFIG_HOME", &config_home);
1714+
let _home_guard = EnvVarGuard::set("HOME", &home);
1715+
let _data_guard =
1716+
EnvVarGuard::set(crate::config::USER_DATA_DIR_ENV, dir.path().join("profile"));
1717+
let _path_guard = EnvVarGuard::set("PATH", &fake_bin);
1718+
let _log_guard = EnvVarGuard::set("TRACEDECAY_SYSTEMCTL_LOG", &log);
1719+
let service_path = config_home
1720+
.join("systemd/user")
1721+
.join(crate::daemon::SERVICE_NAME);
1722+
std::fs::create_dir_all(service_path.parent().expect("service parent"))
1723+
.expect("service dir");
1724+
let original_unit =
1725+
"[Service]\nExecStart=/old/tracedecay daemon run --socket /custom/tracedecay.sock\n";
1726+
std::fs::write(&service_path, original_unit).expect("existing service unit");
1727+
1728+
super::restore_quiesced_installed_service(DaemonServiceState::RunningEnabled)
1729+
.expect("restore service");
1730+
1731+
assert_eq!(
1732+
std::fs::read_to_string(service_path).expect("service unit"),
1733+
original_unit
1734+
);
1735+
assert_eq!(
1736+
std::fs::read_to_string(log).expect("systemctl log"),
1737+
"--user start tracedecay.service\n"
1738+
);
1739+
}
1740+
16221741
#[cfg(target_os = "linux")]
16231742
#[test]
16241743
fn refresh_installed_service_preserves_stopped_state() {

0 commit comments

Comments
 (0)