An extension method to the Topshelf ServiceConfigurator<T>
class that adds Leader checking to the service startup.
Use it when your services require active / passive or any form of high availablility where the services aren't able to naturally compete.
- Catch up suscriptions that don't allow competing consumers
- Services that can operate in an Active / Passive configuration
Don't use this extension if you want a non-leader service to perform tasks whilst the leader service is also performing tasks. The design of the extension is that only one service is actively doing anything at any one time.
Install-Package Topshelf.Leader
Once the package is installed, create a Console application and wireup the Topshelf service as you normally would except for the WhenStarted()
method - this should no longer be used.
You should use the WhenStartedAsLeader()
method that Topshelf.Leader provides instead which constains its own version of the WhenStarted()
method, one with cancellation token support.
using Topshelf.Leader;
public class Program
{
static void Main(string[] args)
{
var rc = HostFactory.Run(x =>
{
x.Service<TheService>(s =>
{
s.WhenStartedAsLeader(builder =>
{
builder.WhenStarted(async (service, token) =>
{
await service.Start(token);
});
builder.WithHeartBeat(
TimeSpan.FromSeconds(30),
(isLeader, token) =>
{
// Track metrics here
return Task.CompletedTask;
});
});
s.ConstructUsing(name => new TheService());
s.WhenStopped(service => service.Stop());
});
});
}
}
The WhenStarted()
method will be executed when the service discovers that it is the current leader. If that situation changes the cancellation token will be set to cancelled. You decide how to handle this situation, throw an exception, exit gracefully or even carry on whatever you were doing - that's entirely up to you.
public class TestService
{
public async Task Start(CancellationToken stopToken)
{
while (!stopToken.IsCancellationRequested)
{
// do your work here, if it's async pass the stopToken to it
}
}
public void Stop()
{
// Tidy up unmanaged resources
}
}
The responsibility for deciding if your service is the leader is delegated to any class which implements the ILeaseManager
interface. The process is
as follows:
- The process will call ILeaseManager.AcquireLease() until we have obtained a lease (which means that we are the leader)
- If we are the leader, the delegate passed to the WhenStarted() builder method is run.
- Whilst we are running the ILeaseManager.RenewLease() method is called.
- When asked to stop the service we call ILeaseManager.ReleaseLease()
You configure which manager to use during the configuration stage. If one isn't supplied then an in memory manager is used. The in memory manager is not muti-process aware so it is not suitable for production use.
Consider using the Topshelf.Leader.AzureBlob lease manager for production environments. It is backed by Azure Blob Storage and is perfectly suited to production loads. If that doesn't meet your requirements it's easy to write your own.
var rc = HostFactory.Run(x =>
{
x.Service<TheService>(s =>
{
s.WhenStartedAsLeader(builder =>
{
builder.WhenStarted(async (service, token) =>
{
await service.Start(token);
});
builder.Lease(lcb =>
{
lcb.RenewLeaseEvery(TimeSpan.FromSeconds(2))
.AquireLeaseEvery(TimeSpan.FromSeconds(5))
.LeaseLength(TimeSpan.FromDays(1))
.WithLeaseManager(new YourManagerHere());
});
});
s.ConstructUsing(name => new TheService());
s.WhenStopped(service => service.Stop());
});
}
public class InMemoryLeaseManager : ILeaseManager
{
private string owningNodeId;
public InMemoryLeaseManager(string owningNodeId)
{
this.owningNodeId = owningNodeId;
}
public void AssignLeader(string newLeaderId)
{
this.owningNodeId = newLeaderId;
}
public Task<bool> AcquireLease(LeaseOptions options, CancellationToken token)
{
return Task.FromResult(options.NodeId == owningNodeId);
}
public Task<bool> RenewLease(LeaseOptions options, CancellationToken token)
{
return Task.FromResult(options.NodeId == owningNodeId);
}
public Task ReleaseLease(LeaseReleaseOptions options)
{
owningNodeId = string.Empty;
return Task.FromResult(true);
}
}