Skip to content

Commit 0607044

Browse files
committed
Acquire rewatch build lock before initialization (#8409)
* Acquire rewatch build lock before initialization * Satisfy clippy * CHANGELOG # Conflicts: # CHANGELOG.md # rewatch/src/build.rs
1 parent 509cff1 commit 0607044

2 files changed

Lines changed: 151 additions & 50 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- Rewatch: preserve warnings after atomic-save full rebuilds. https://github.com/rescript-lang/rescript/pull/8358
3131
- Fix type lowering for `dict{}` and `async`, so you don't need to annotate one extra time when the type is known. https://github.com/rescript-lang/rescript/pull/8359
3232
- Fix issue where warning 56 would blow up with `dict{}` patterns. https://github.com/rescript-lang/rescript/pull/8403
33+
- Acquire rewatch build lock before initialization. https://github.com/rescript-lang/rescript/pull/8409
3334

3435
#### :nail_care: Polish
3536

rewatch/src/build.rs

Lines changed: 150 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,14 @@ impl fmt::Display for IncrementalBuildError {
236236
}
237237
}
238238

239+
fn with_build_lock<T>(path: &Path, f: impl FnOnce() -> T) -> T {
240+
let build_folder = path.to_string_lossy().to_string();
241+
let _lock = get_lock_or_exit(LockKind::Build, &build_folder);
242+
let result = f();
243+
let _lock = drop_lock(LockKind::Build, &build_folder);
244+
result
245+
}
246+
239247
pub fn incremental_build(
240248
build_state: &mut BuildCommandState,
241249
default_timing: Option<Duration>,
@@ -245,10 +253,31 @@ pub fn incremental_build(
245253
create_sourcedirs: bool,
246254
plain_output: bool,
247255
) -> Result<(), IncrementalBuildError> {
248-
let build_folder = build_state.root_folder.to_string_lossy().to_string();
249-
250-
let _lock = get_lock_or_exit(LockKind::Build, &build_folder);
256+
let build_folder = build_state.root_folder.clone();
257+
with_build_lock(&build_folder, || {
258+
incremental_build_without_lock(
259+
build_state,
260+
default_timing,
261+
initial_build,
262+
show_progress,
263+
only_incremental,
264+
create_sourcedirs,
265+
plain_output,
266+
)
267+
})
268+
}
251269

270+
// `build` needs to lock before initialization because initialization mutates previous
271+
// build artifacts. Keep the compile body separate so it can run under that wider lock.
272+
fn incremental_build_without_lock(
273+
build_state: &mut BuildCommandState,
274+
default_timing: Option<Duration>,
275+
initial_build: bool,
276+
show_progress: bool,
277+
only_incremental: bool,
278+
create_sourcedirs: bool,
279+
plain_output: bool,
280+
) -> Result<(), IncrementalBuildError> {
252281
logs::initialize(&build_state.packages);
253282
let num_dirty_modules = build_state.modules.values().filter(|m| is_dirty(m)).count() as u64;
254283
let pb = if !plain_output && show_progress {
@@ -292,7 +321,6 @@ pub fn incremental_build(
292321
}
293322

294323
eprintln!("{}", &err);
295-
let _lock = drop_lock(LockKind::Build, &build_folder);
296324

297325
return Err(IncrementalBuildError {
298326
kind: IncrementalBuildErrorKind::SourceFileParseError,
@@ -356,13 +384,9 @@ pub fn incremental_build(
356384
|| pb.inc(1),
357385
|size| pb.set_length(size),
358386
)
359-
.map_err(|e| {
360-
let _lock = drop_lock(LockKind::Build, &build_folder);
361-
362-
IncrementalBuildError {
363-
kind: IncrementalBuildErrorKind::CompileError(Some(e.to_string())),
364-
plain_output,
365-
}
387+
.map_err(|e| IncrementalBuildError {
388+
kind: IncrementalBuildErrorKind::CompileError(Some(e.to_string())),
389+
plain_output,
366390
})?;
367391

368392
let compile_duration = start_compiling.elapsed();
@@ -397,8 +421,6 @@ pub fn incremental_build(
397421
eprintln!("{}", &compile_errors);
398422
}
399423

400-
let _lock = drop_lock(LockKind::Build, &build_folder);
401-
402424
Err(IncrementalBuildError {
403425
kind: IncrementalBuildErrorKind::CompileError(None),
404426
plain_output,
@@ -429,7 +451,6 @@ pub fn incremental_build(
429451
// Write per-package compiler metadata to `lib/bs/compiler-info.json` (idempotent)
430452
write_compiler_info(build_state);
431453

432-
let _lock = drop_lock(LockKind::Build, &build_folder);
433454
Ok(())
434455
}
435456
}
@@ -529,43 +550,122 @@ pub fn build(
529550
None
530551
};
531552
let timing_total = Instant::now();
532-
let mut build_state = initialize_build(
533-
default_timing,
534-
filter,
535-
show_progress,
536-
path,
537-
plain_output,
538-
warn_error,
539-
)
540-
.with_context(|| "Could not initialize build")?;
541-
542-
match incremental_build(
543-
&mut build_state,
544-
default_timing,
545-
true,
546-
show_progress,
547-
false,
548-
create_sourcedirs,
549-
plain_output,
550-
) {
551-
Ok(_) => {
552-
if !plain_output && show_progress {
553-
let timing_total_elapsed = timing_total.elapsed();
554-
println!(
555-
"\n{}{}Finished Compilation in {:.2}s",
556-
LINE_CLEAR,
557-
SPARKLES,
558-
default_timing.unwrap_or(timing_total_elapsed).as_secs_f64()
559-
);
553+
with_build_lock(path, || {
554+
let mut build_state = initialize_build(
555+
default_timing,
556+
filter,
557+
show_progress,
558+
path,
559+
plain_output,
560+
warn_error,
561+
)
562+
.with_context(|| "Could not initialize build")?;
563+
564+
match incremental_build_without_lock(
565+
&mut build_state,
566+
default_timing,
567+
true,
568+
show_progress,
569+
false,
570+
create_sourcedirs,
571+
plain_output,
572+
) {
573+
Ok(_) => {
574+
if !plain_output && show_progress {
575+
let timing_total_elapsed = timing_total.elapsed();
576+
println!(
577+
"\n{}{}Finished Compilation in {:.2}s",
578+
LINE_CLEAR,
579+
SPARKLES,
580+
default_timing.unwrap_or(timing_total_elapsed).as_secs_f64()
581+
);
582+
}
583+
clean::cleanup_after_build(&build_state);
584+
write_build_ninja(&build_state);
585+
Ok(build_state)
586+
}
587+
Err(e) => {
588+
clean::cleanup_after_build(&build_state);
589+
write_build_ninja(&build_state);
590+
Err(anyhow!("Incremental build failed. Error: {e}"))
560591
}
561-
clean::cleanup_after_build(&build_state);
562-
write_build_ninja(&build_state);
563-
Ok(build_state)
564-
}
565-
Err(e) => {
566-
clean::cleanup_after_build(&build_state);
567-
write_build_ninja(&build_state);
568-
Err(anyhow!("Incremental build failed. Error: {e}"))
569592
}
593+
})
594+
}
595+
#[cfg(test)]
596+
mod tests {
597+
use super::*;
598+
use std::fs;
599+
use std::process;
600+
use std::sync::mpsc;
601+
use std::thread;
602+
use tempfile::TempDir;
603+
604+
#[test]
605+
fn with_build_lock_holds_lock_while_running_work() {
606+
let temp_dir = TempDir::new().expect("temp dir should be created");
607+
let project_folder = temp_dir.path();
608+
let build_lock_path = project_folder.join("lib").join(LockKind::Build.file_name());
609+
610+
let result = with_build_lock(project_folder, || {
611+
assert!(
612+
build_lock_path.exists(),
613+
"build lock should be held while guarded work runs"
614+
);
615+
42
616+
});
617+
618+
assert_eq!(result, 42);
619+
assert!(
620+
!build_lock_path.exists(),
621+
"build lock should be removed after guarded work finishes"
622+
);
623+
}
624+
625+
#[test]
626+
fn with_build_lock_drops_lock_after_error_result() {
627+
let temp_dir = TempDir::new().expect("temp dir should be created");
628+
let project_folder = temp_dir.path();
629+
let build_lock_path = project_folder.join("lib").join(LockKind::Build.file_name());
630+
631+
let result: Result<(), &str> = with_build_lock(project_folder, || Err("failed"));
632+
633+
assert_eq!(result, Err("failed"));
634+
assert!(
635+
!build_lock_path.exists(),
636+
"build lock should be removed after guarded work returns an error"
637+
);
638+
}
639+
640+
#[test]
641+
fn build_waits_for_lock_before_initializing() {
642+
let temp_dir = TempDir::new().expect("temp dir should be created");
643+
let project_folder = temp_dir.path().to_path_buf();
644+
let lib_dir = project_folder.join("lib");
645+
let build_lock_path = lib_dir.join(LockKind::Build.file_name());
646+
fs::create_dir_all(&lib_dir).expect("lib directory should be created");
647+
fs::write(&build_lock_path, process::id().to_string()).expect("lockfile should be written");
648+
649+
let (sender, receiver) = mpsc::channel();
650+
let build_project_folder = project_folder.clone();
651+
let build_thread = thread::spawn(move || {
652+
// This temp project has no config, so initialization would fail immediately
653+
// if `build` did not wait for the lock first.
654+
let result = build(&None, &build_project_folder, false, true, false, true, None);
655+
sender.send(result.is_err()).expect("result should be sent");
656+
});
657+
658+
assert!(
659+
receiver.recv_timeout(Duration::from_millis(250)).is_err(),
660+
"build should wait for build.lock before running initialization"
661+
);
662+
663+
fs::remove_file(&build_lock_path).expect("lockfile should be removed");
664+
assert!(
665+
receiver
666+
.recv_timeout(Duration::from_secs(5))
667+
.expect("build should finish after lock is removed")
668+
);
669+
build_thread.join().expect("build thread should complete");
570670
}
571671
}

0 commit comments

Comments
 (0)