Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Calamari.Common.Features.Processes.Semaphores;
using Calamari.Common.Features.Processes.NamedLocks;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.Logging;
using Calamari.Common.Plumbing.Variables;
Expand All @@ -12,16 +12,16 @@ namespace Calamari.Common.Features.Deployment.Journal
{
public class DeploymentJournal : IDeploymentJournal
{
const string SemaphoreName = "Octopus.Calamari.DeploymentJournal";
const string NamedLockName = "Octopus.Calamari.DeploymentJournal";
readonly ICalamariFileSystem fileSystem;
readonly ISemaphoreFactory semaphore;
readonly INamedLockManager namedLockManager;
readonly IVariables variables;
readonly ILog log;

public DeploymentJournal(ICalamariFileSystem fileSystem, ISemaphoreFactory semaphore, IVariables variables, ILog log)
public DeploymentJournal(ICalamariFileSystem fileSystem, INamedLockManager namedLockManager, IVariables variables, ILog log)
{
this.fileSystem = fileSystem;
this.semaphore = semaphore;
this.namedLockManager = namedLockManager;
this.variables = variables;
this.log = log;
}
Expand All @@ -30,7 +30,7 @@ public DeploymentJournal(ICalamariFileSystem fileSystem, ISemaphoreFactory semap

internal void AddJournalEntry(JournalEntry entry)
{
using (semaphore.Acquire(SemaphoreName, "Another process is using the deployment journal"))
using (namedLockManager.Acquire(NamedLockName, "Another process is using the deployment journal"))
{
var xElement = entry.ToXmlElement();
log.VerboseFormat("Adding journal entry:\n{0}", xElement.ToString());
Expand All @@ -40,15 +40,15 @@ internal void AddJournalEntry(JournalEntry entry)

public List<JournalEntry> GetAllJournalEntries()
{
using (semaphore.Acquire(SemaphoreName, "Another process is using the deployment journal"))
using (namedLockManager.Acquire(NamedLockName, "Another process is using the deployment journal"))
{
return Read().Select(element => new JournalEntry(element)).ToList();
}
}

public void RemoveJournalEntries(IEnumerable<string> ids)
{
using (semaphore.Acquire(SemaphoreName, "Another process is using the deployment journal"))
using (namedLockManager.Acquire(NamedLockName, "Another process is using the deployment journal"))
{
var elements = Read();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System;

namespace Calamari.Common.Features.Processes.Semaphores
namespace Calamari.Common.Features.Processes.NamedLocks
{
public interface ISemaphoreFactory
public interface INamedLockManager
{
IDisposable Acquire(string name, string waitMessage);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using System;
using System.Runtime.ExceptionServices;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Threading;
using Calamari.Common.Plumbing.Extensions;
using Calamari.Common.Plumbing.Logging;
using Polly;
using Polly.Retry;

namespace Calamari.Common.Features.Processes.NamedLocks
{
public class MutexBasedNamedLockManager : INamedLockManager
{
readonly ILog log;
readonly int initialWaitBeforeShowingLogMessage;
readonly ResiliencePipeline mutexCreationPipeline;

public MutexBasedNamedLockManager()
{
log = ConsoleLog.Instance;
initialWaitBeforeShowingLogMessage = (int)TimeSpan.FromSeconds(3).TotalMilliseconds;

mutexCreationPipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions()
{
ShouldHandle = new PredicateBuilder().Handle<Exception>(),
MaxRetryAttempts = 5, //means we'll wait for a max of around 250ms
BackoffType = DelayBackoffType.Linear,
UseJitter = true,
Delay = TimeSpan.FromMilliseconds(50),
OnRetry = args =>
{
log.Verbose($"Waiting {args.RetryDelay.TotalMilliseconds}ms before attempting to create the Mutex again");
return default;
}
})
.Build();
}

public IDisposable Acquire(string name, string waitMessage)
{
log.Verbose($"Acquiring named lock for {name}");

var trackedThread = new TrackedThread($"Mutex owner for '{name}'",
tracker =>
{
var globalName = $@"Global\{name}";

Mutex? mutex = null;
try
{
// Create/acquire the global mutex with some retry, to (hopefully) avoid two instances of
// Calamari racing to create it (e.g. parallel steps on the same machine)
mutex = mutexCreationPipeline.Execute(() => new Mutex(false, globalName));


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think adding some verbose logging into where we get up to would be handy to diagnose issues where we see calamari seemingly hang, I think one before and after mutex create. And one just before the attempt to WaitOne. That ill let us know does it reach creation, could it create it, could it get past user stuff?

Oh maybe also a "mutex acquired" one would be good.

// Assign full control for all users, so that a lock taken by (say) a Tentacle running as a service
// is still accessible to Calamari running under a different account
if (OperatingSystem.IsWindows())
{
SetFullAccessControlForAllUsers(mutex, globalName);
log.Verbose($"Calamari mutex configured to allow control for all users on '{name}'");
}

try
{
log.Verbose($"Attempting to acquire mutex on Calamari '{name}'");
if (!mutex.WaitOne(initialWaitBeforeShowingLogMessage))
{
log.Verbose(waitMessage);
mutex.WaitOne();
}
}
catch (AbandonedMutexException)
{
// The previous owner died without releasing; the kernel has handed ownership to us
log.Warn($"The mutex '{name}' was abandoned by a previous process that exited without releasing it. Continuing, but anything it was protecting may have been left in an inconsistent state.");
}
}
catch (Exception ex)
{
// We never took the mutex, so there is nothing to release - just
// close the handle rather than leaving it to the finaliser
mutex?.Dispose();
tracker.MarkAsErrored(ex);
return;
}

log.Verbose($"Acquired lock on Calamari for '{name}'");
tracker.HoldUntilDisposed();

// An unhandled exception here would terminate the process. Releasing is best effort:
// a failure leaves the mutex abandoned, which the next waiter recovers from.
try
{
try
{
mutex.ReleaseMutex();
}
finally
{
mutex.Dispose();
}
}
catch (Exception ex)
{
log.Verbose($"Failed to release the mutex '{globalName}': {ex.PrettyPrint()}");
}
});

trackedThread.StartAndBlockUntilLockAcquired();
return trackedThread;
}

[SupportedOSPlatform("windows")]
void SetFullAccessControlForAllUsers(Mutex mutex, string name)
{
var mutexSecurity = new MutexSecurity();
var everyone = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
var rule = new MutexAccessRule(everyone, MutexRights.FullControl, AccessControlType.Allow);

mutexSecurity.AddAccessRule(rule);

try
{
mutex.SetAccessControl(mutexSecurity);
}
catch (Exception e)
{
log.Verbose($"Failed to set access controls on mutex '{name}': {e.PrettyPrint()}");
}
}

class TrackedThread : IDisposable
{
readonly ManualResetEventSlim acquired = new(false);
readonly ManualResetEventSlim release = new(false);
readonly Thread owner;

int released;
Exception? error;

public TrackedThread(string name, Action<TrackedThread> threadStart)
{
owner = new Thread(() => threadStart(this))
{
IsBackground = true,
Name = name
};
}

public void StartAndBlockUntilLockAcquired()
{
owner.Start();

// Wait for the owner thread to signal it has "entered", or errored
acquired.Wait();

if (error != null)
{
Dispose();
ExceptionDispatchInfo.Capture(error).Throw();
}
}

public void HoldUntilDisposed()
{
acquired.Set();
release.Wait();
}

public void MarkAsErrored(Exception ex)
{
error = ex;
acquired.Set();
}

public void Dispose()
{
// The events are gone after the first time through, so guard against a caller disposing twice
if (Interlocked.Exchange(ref released, 1) != 0)
return;

release.Set();
owner.Join();
acquired.Dispose();
release.Dispose();
Comment on lines +186 to +189

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess calamari is a throw away process so it wont matter, but we probably should try finally each one of these.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I threw fable at this, because I'm not super well versed in what can be thrown here. Turns out nothing that we haven't already guarded against, I'm comfortable with this as-is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, that's fine

}
}
}
}
Loading