Browse Source
* Purge hanging replicas * Use dictionary for event serialization * Subscribing to replica events instead of waiting for arbitrary time in tests * Only return list of running containers from DockerAssert * use IDictionary instead of deserializing to ReplicaStatus when removing stale replicas * Exit logs loop in DockerRunner if container is killeddavidfowl/dependencies
committed by
GitHub
8 changed files with 486 additions and 12 deletions
@ -0,0 +1,104 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Text.Json; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Tye.Hosting.Model; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using System.Threading; |
||||
|
using System.Collections.Concurrent; |
||||
|
|
||||
|
namespace Microsoft.Tye.Hosting |
||||
|
{ |
||||
|
public class ReplicaRegistry : IDisposable |
||||
|
{ |
||||
|
private readonly ILogger _logger; |
||||
|
private readonly ConcurrentDictionary<string, SemaphoreSlim> _fileWriteSemaphores; |
||||
|
private readonly string _tyeFolderPath; |
||||
|
|
||||
|
public ReplicaRegistry(Model.Application application, ILogger logger) |
||||
|
{ |
||||
|
_logger = logger; |
||||
|
_fileWriteSemaphores = new ConcurrentDictionary<string, SemaphoreSlim>(); |
||||
|
_tyeFolderPath = Path.Join(Path.GetDirectoryName(application.Source), ".tye"); |
||||
|
|
||||
|
if (!Directory.Exists(_tyeFolderPath)) |
||||
|
{ |
||||
|
Directory.CreateDirectory(_tyeFolderPath); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool WriteReplicaEvent(string storeName, IDictionary<string, string> replicaRecord) |
||||
|
{ |
||||
|
var filePath = Path.Join(_tyeFolderPath, GetStoreFile(storeName)); |
||||
|
var contents = JsonSerializer.Serialize(replicaRecord, new JsonSerializerOptions { WriteIndented = false }); |
||||
|
var semaphore = GetSempahoreForStore(storeName); |
||||
|
|
||||
|
semaphore.Wait(); |
||||
|
try |
||||
|
{ |
||||
|
File.AppendAllText(filePath, contents + Environment.NewLine); |
||||
|
return true; |
||||
|
} |
||||
|
catch (DirectoryNotFoundException ex) |
||||
|
{ |
||||
|
_logger.LogWarning(ex, "tye folder is not found. file: {file}", filePath); |
||||
|
return false; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
semaphore.Release(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool DeleteStore(string storeName) |
||||
|
{ |
||||
|
var filePath = Path.Join(_tyeFolderPath, GetStoreFile(storeName)); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
File.Delete(storeName); |
||||
|
return true; |
||||
|
} |
||||
|
catch (DirectoryNotFoundException ex) |
||||
|
{ |
||||
|
_logger.LogWarning(ex, "tye folder is not found. file: {file}", filePath); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async ValueTask<IList<IDictionary<string, string>>> GetEvents(string storeName) |
||||
|
{ |
||||
|
var filePath = Path.Join(_tyeFolderPath, GetStoreFile(storeName)); |
||||
|
|
||||
|
if (!File.Exists(filePath)) |
||||
|
{ |
||||
|
return Array.Empty<IDictionary<string, string>>(); |
||||
|
} |
||||
|
|
||||
|
var contents = await File.ReadAllTextAsync(filePath); |
||||
|
var events = contents.Split(Environment.NewLine); |
||||
|
|
||||
|
return events.Where(e => !string.IsNullOrEmpty(e.Trim())) |
||||
|
.Select(e => JsonSerializer.Deserialize<IDictionary<string, string>>(e)) |
||||
|
.ToList(); |
||||
|
} |
||||
|
|
||||
|
private SemaphoreSlim GetSempahoreForStore(string storeName) |
||||
|
{ |
||||
|
return _fileWriteSemaphores.GetOrAdd(storeName, _ => new SemaphoreSlim(1, 1)); |
||||
|
} |
||||
|
|
||||
|
private string GetStoreFile(string storeName) => $"{storeName}_store"; |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
Directory.Delete(_tyeFolderPath, true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,155 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.CommandLine; |
||||
|
using System.Diagnostics; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Hosting; |
||||
|
using Microsoft.Tye; |
||||
|
using Microsoft.Tye.ConfigModel; |
||||
|
using Microsoft.Tye.Hosting; |
||||
|
using Microsoft.Tye.Hosting.Model; |
||||
|
using Xunit; |
||||
|
using Xunit.Abstractions; |
||||
|
|
||||
|
namespace E2ETest |
||||
|
{ |
||||
|
public class TyePurgeTests |
||||
|
{ |
||||
|
private readonly ITestOutputHelper output; |
||||
|
private readonly TestOutputLogEventSink sink; |
||||
|
|
||||
|
public TyePurgeTests(ITestOutputHelper output) |
||||
|
{ |
||||
|
this.output = output; |
||||
|
sink = new TestOutputLogEventSink(output); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FrontendBackendPurgeTest() |
||||
|
{ |
||||
|
var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "frontend-backend")); |
||||
|
using var tempDirectory = TempDirectory.Create(); |
||||
|
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); |
||||
|
|
||||
|
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); |
||||
|
var tyeDir = new DirectoryInfo(Path.Combine(tempDirectory.DirectoryPath, ".tye")); |
||||
|
var outputContext = new OutputContext(sink, Verbosity.Debug); |
||||
|
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); |
||||
|
var host = new TyeHost(application.ToHostingApplication(), Array.Empty<string>()) |
||||
|
{ |
||||
|
Sink = sink, |
||||
|
}; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await TestHelpers.StartHostAndWaitForReplicasToStart(host); |
||||
|
try |
||||
|
{ |
||||
|
var pids = GetAllAppPids(host.Application); |
||||
|
|
||||
|
Assert.True(Directory.Exists(tyeDir.FullName)); |
||||
|
Assert.Subset(new HashSet<int>(GetAllPids()), new HashSet<int>(pids)); |
||||
|
|
||||
|
await TestHelpers.PurgeHostAndWaitForGivenReplicasToStop(host, |
||||
|
GetAllReplicasNames(host.Application), tyeDir.FullName); |
||||
|
|
||||
|
var runningPids = new HashSet<int>(GetAllPids()); |
||||
|
Assert.True(pids.All(pid => !runningPids.Contains(pid))); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
await host.StopAsync(); |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
host.Dispose(); |
||||
|
Assert.False(Directory.Exists(tyeDir.FullName)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[ConditionalFact] |
||||
|
[SkipIfDockerNotRunning] |
||||
|
public async Task MultiProjectPurgeTest() |
||||
|
{ |
||||
|
var projectDirectory = new DirectoryInfo(Path.Combine(TestHelpers.GetSolutionRootDirectory("tye"), "samples", "multi-project")); |
||||
|
using var tempDirectory = TempDirectory.Create(); |
||||
|
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath); |
||||
|
|
||||
|
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml")); |
||||
|
var tyeDir = new DirectoryInfo(Path.Combine(tempDirectory.DirectoryPath, ".tye")); |
||||
|
var outputContext = new OutputContext(sink, Verbosity.Debug); |
||||
|
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); |
||||
|
var host = new TyeHost(application.ToHostingApplication(), Array.Empty<string>()) |
||||
|
{ |
||||
|
Sink = sink, |
||||
|
}; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await TestHelpers.StartHostAndWaitForReplicasToStart(host); |
||||
|
try |
||||
|
{ |
||||
|
var pids = GetAllAppPids(host.Application); |
||||
|
var containers = GetAllContainerIds(host.Application); |
||||
|
|
||||
|
Assert.True(Directory.Exists(tyeDir.FullName)); |
||||
|
Assert.Subset(new HashSet<int>(GetAllPids()), new HashSet<int>(pids)); |
||||
|
Assert.Subset(new HashSet<string>(await DockerAssert.GetRunningContainersIdsAsync(output)), |
||||
|
new HashSet<string>(containers)); |
||||
|
|
||||
|
await TestHelpers.PurgeHostAndWaitForGivenReplicasToStop(host, |
||||
|
GetAllReplicasNames(host.Application), tyeDir.FullName); |
||||
|
|
||||
|
var runningPids = new HashSet<int>(GetAllPids()); |
||||
|
Assert.True(pids.All(pid => !runningPids.Contains(pid))); |
||||
|
var runningContainers = |
||||
|
new HashSet<string>(await DockerAssert.GetRunningContainersIdsAsync(output)); |
||||
|
Assert.True(containers.All(c => !runningContainers.Contains(c))); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
await host.StopAsync(); |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
host.Dispose(); |
||||
|
Assert.False(Directory.Exists(tyeDir.FullName)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private string[] GetAllReplicasNames(Microsoft.Tye.Hosting.Model.Application application) |
||||
|
{ |
||||
|
var replicas = application.Services.SelectMany(s => s.Value.Replicas); |
||||
|
return replicas.Select(r => r.Value.Name).ToArray(); |
||||
|
} |
||||
|
|
||||
|
private int[] GetAllAppPids(Microsoft.Tye.Hosting.Model.Application application) |
||||
|
{ |
||||
|
var replicas = application.Services.SelectMany(s => s.Value.Replicas); |
||||
|
var ids = replicas.Where(r => r.Value is ProcessStatus).Select(r => ((ProcessStatus)r.Value).Pid ?? -1).ToArray(); |
||||
|
|
||||
|
return ids; |
||||
|
} |
||||
|
|
||||
|
private string[] GetAllContainerIds(Microsoft.Tye.Hosting.Model.Application application) |
||||
|
{ |
||||
|
var replicas = application.Services.SelectMany(s => s.Value.Replicas); |
||||
|
var ids = replicas.Where(r => r.Value is DockerStatus).Select(r => ((DockerStatus)r.Value).ContainerId!).ToArray(); |
||||
|
|
||||
|
return ids; |
||||
|
} |
||||
|
|
||||
|
private int[] GetAllPids() |
||||
|
{ |
||||
|
return Process.GetProcesses().Select(p => p.Id).ToArray(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue