1717import static com .google .common .collect .ImmutableMap .toImmutableMap ;
1818import static com .google .common .truth .Truth .assertThat ;
1919import static java .nio .charset .StandardCharsets .UTF_8 ;
20+ import static java .util .concurrent .TimeUnit .SECONDS ;
2021
2122import com .google .common .collect .ImmutableList ;
2223import com .google .common .collect .ImmutableMap ;
3132import com .google .devtools .build .lib .exec .util .SpawnBuilder ;
3233import com .google .devtools .build .lib .sandbox .SandboxHelpers .SandboxInputs ;
3334import com .google .devtools .build .lib .testutil .Scratch ;
35+ import com .google .devtools .build .lib .testutil .TestUtils ;
36+ import com .google .devtools .build .lib .vfs .DigestHashFunction ;
3437import com .google .devtools .build .lib .vfs .Dirent ;
38+ import com .google .devtools .build .lib .vfs .FileSystem ;
3539import com .google .devtools .build .lib .vfs .FileSystemUtils ;
3640import com .google .devtools .build .lib .vfs .Path ;
3741import com .google .devtools .build .lib .vfs .PathFragment ;
3842import com .google .devtools .build .lib .vfs .Symlinks ;
43+ import com .google .devtools .build .lib .vfs .inmemoryfs .InMemoryFileSystem ;
3944import java .io .IOException ;
4045import java .util .Arrays ;
46+ import java .util .concurrent .BrokenBarrierException ;
47+ import java .util .concurrent .CyclicBarrier ;
48+ import java .util .concurrent .ExecutorService ;
49+ import java .util .concurrent .Executors ;
50+ import java .util .concurrent .Future ;
51+ import java .util .concurrent .Semaphore ;
4152import java .util .function .Function ;
53+ import javax .annotation .Nullable ;
54+ import org .junit .After ;
4255import org .junit .Before ;
4356import org .junit .Test ;
4457import org .junit .runner .RunWith ;
@@ -53,12 +66,23 @@ public class SandboxHelpersTest {
5366
5467 private final Scratch scratch = new Scratch ();
5568 private Path execRoot ;
69+ @ Nullable private ExecutorService executorToCleanup ;
5670
5771 @ Before
5872 public void createExecRoot () throws IOException {
5973 execRoot = scratch .dir ("/execRoot" );
6074 }
6175
76+ @ After
77+ public void shutdownExecutor () throws InterruptedException {
78+ if (executorToCleanup == null ) {
79+ return ;
80+ }
81+
82+ executorToCleanup .shutdown ();
83+ executorToCleanup .awaitTermination (TestUtils .WAIT_TIMEOUT_SECONDS , SECONDS );
84+ }
85+
6286 @ Test
6387 public void processInputFiles_materializesParamFile () throws Exception {
6488 SandboxHelpers sandboxHelpers = new SandboxHelpers (/*delayVirtualInputMaterialization=*/ false );
@@ -150,6 +174,56 @@ public void sandboxInputMaterializeVirtualInputs_delayMaterialization_writesCorr
150174 assertThat (sandboxToolFile .isExecutable ()).isTrue ();
151175 }
152176
177+ /**
178+ * Test simulating a scenario when 2 parallel writes of the same virtual input both complete write
179+ * of the temp file and then proceed with post-processing steps one-by-one.
180+ */
181+ @ Test
182+ public void sandboxInputMaterializeVirtualInput_parallelWritesForSameInput_writesCorrectFile ()
183+ throws Exception {
184+ VirtualActionInput input = ActionsTestUtil .createVirtualActionInput ("file" , "hello" );
185+ executorToCleanup = Executors .newSingleThreadExecutor ();
186+ CyclicBarrier bothWroteTempFile = new CyclicBarrier (2 );
187+ Semaphore finishProcessingSemaphore = new Semaphore (1 );
188+ FileSystem customFs =
189+ new InMemoryFileSystem (DigestHashFunction .SHA1 ) {
190+ @ Override
191+ protected void setExecutable (Path path , boolean executable ) throws IOException {
192+ try {
193+ bothWroteTempFile .await ();
194+ finishProcessingSemaphore .acquire ();
195+ } catch (BrokenBarrierException | InterruptedException e ) {
196+ throw new IllegalArgumentException (e );
197+ }
198+ super .setExecutable (path , executable );
199+ }
200+ };
201+ Scratch customScratch = new Scratch (customFs );
202+ Path customExecRoot = customScratch .dir ("/execroot" );
203+ SandboxHelpers sandboxHelpers = new SandboxHelpers (/*delayVirtualInputMaterialization=*/ false );
204+
205+ Future <?> future =
206+ executorToCleanup .submit (
207+ () -> {
208+ try {
209+ sandboxHelpers .processInputFiles (
210+ inputMap (input ), SPAWN , EMPTY_EXPANDER , customExecRoot );
211+ finishProcessingSemaphore .release ();
212+ } catch (IOException e ) {
213+ throw new IllegalArgumentException (e );
214+ }
215+ });
216+ sandboxHelpers .processInputFiles (inputMap (input ), SPAWN , EMPTY_EXPANDER , customExecRoot );
217+ finishProcessingSemaphore .release ();
218+ future .get ();
219+
220+ assertThat (customExecRoot .readdir (Symlinks .NOFOLLOW ))
221+ .containsExactly (new Dirent ("file" , Dirent .Type .FILE ));
222+ Path outputFile = customExecRoot .getChild ("file" );
223+ assertThat (FileSystemUtils .readLines (outputFile , UTF_8 )).containsExactly ("hello" );
224+ assertThat (outputFile .isExecutable ()).isTrue ();
225+ }
226+
153227 private static ImmutableMap <PathFragment , ActionInput > inputMap (ActionInput ... inputs ) {
154228 return Arrays .stream (inputs )
155229 .collect (toImmutableMap (ActionInput ::getExecPath , Function .identity ()));
0 commit comments