Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/Runner.Worker/ExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1330,7 +1330,11 @@ public void WriteWebhookPayload()
{
var workflowFile = Path.Combine(workflowDirectory, "event.json");
Trace.Info($"Write event payload to {workflowFile}");
File.WriteAllText(workflowFile, gitHubEvent, new UTF8Encoding(false));

// Ensure "parallel:" does not cause crashes here
var tempEventFile = Path.Combine(workflowDirectory, $"event.{Guid.NewGuid():N}.tmp");
File.WriteAllText(tempEventFile, gitHubEvent, new UTF8Encoding(false));
File.Move(tempEventFile, workflowFile, overwrite: true);
Comment on lines +1334 to +1337
SetGitHubContext("event_path", workflowFile);
}
}
Expand Down
83 changes: 83 additions & 0 deletions src/Test/L0/Worker/ExecutionContextL0.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Worker;
Expand Down Expand Up @@ -1014,6 +1017,86 @@ public void PublishStepResult_EmbeddedStep_Legacy()
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task WriteWebhookPayload_ConcurrentReaderNeverObservesTruncatedFile()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange: Create a job request message with a github.event payload large enough
// that a non-atomic rewrite is observable while the stream flushes.
TaskOrchestrationPlanReference plan = new();
TimelineReference timeline = new();
Guid jobId = Guid.NewGuid();
string jobName = "some job name";
var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource()
{
Alias = Pipelines.PipelineConstants.SelfAlias,
Id = "github",
Version = "sha1"
});
string gitHubEvent = $"{{\"filler\":\"{new string('a', 512 * 1024)}\"}}";
var githubData = new Pipelines.ContextData.DictionaryContextData();
githubData["event"] = new StringContextData(gitHubEvent);
jobRequest.ContextData["github"] = githubData;

var pagingLogger = new Mock<IPagingLogger>();
hc.EnqueueInstance(pagingLogger.Object);

var ec = new Runner.Worker.ExecutionContext();
ec.Initialize(hc);
ec.InitializeJob(jobRequest, CancellationToken.None);

ec.WriteWebhookPayload();
string eventPath = ec.GetGitHubContext("event_path");
Assert.Equal(gitHubEvent, File.ReadAllText(eventPath));

// Act: Rewrite the payload the way each starting action step does
// (ActionRunner.RunAsync), while a concurrent reader mimics a sibling parallel
// step's Node action parsing GITHUB_EVENT_PATH at module load.
using var writerDone = new CancellationTokenSource();
var writer = Task.Run(() =>
{
try
{
for (int i = 0; i < 100; i++)
{
ec.WriteWebhookPayload();
}
}
finally
{
writerDone.Cancel();
}
});

var truncatedReadLengths = new List<int>();
var reader = Task.Run(() =>
{
while (!writerDone.IsCancellationRequested)
{
using var stream = new FileStream(eventPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
using var textReader = new StreamReader(stream, new UTF8Encoding(false));
string observed = textReader.ReadToEnd();
if (observed.Length != gitHubEvent.Length)
Comment on lines +1059 to +1083
{
truncatedReadLengths.Add(observed.Length);
}
}
});

await writer;
await reader;

// Assert
Assert.True(truncatedReadLengths.Count == 0, $"{truncatedReadLengths.Count} concurrent read(s) observed a truncated event.json (expected {gitHubEvent.Length} chars, observed lengths: {string.Join(", ", truncatedReadLengths.Take(5))}). The event.json write must be atomic.");
Assert.Equal(gitHubEvent, File.ReadAllText(eventPath));
Assert.Empty(Directory.GetFiles(Path.GetDirectoryName(eventPath), "*.tmp"));
}
}

private TestHostContext CreateTestContext([CallerMemberName] String testName = "")
{
var hc = new TestHostContext(this, testName);
Expand Down