Saturday, August 24, 2024

Leader Election

CREATE TABLE LeaderElection (

    Id INT PRIMARY KEY IDENTITY,

    HostName NVARCHAR(255) NOT NULL,

    Port INT NOT NULL,

    IsLeader BIT NOT NULL,

    LastHeartbeat DATETIME NOT NULL

);

 

public class LeaderElection { public int Id { get; set; } public string HostName { get; set; } public int Port { get; set; } public bool IsLeader { get; set; } public DateTime LastHeartbeat { get; set; } } 



public class LeaderElectionService

{

    private readonly ApplicationDbContext _dbContext;

    private readonly IHttpContextAccessor _httpContextAccessor;

    private string _hostName;

    private int _port;

    private Timer _timer;


    public LeaderElectionService(ApplicationDbContext dbContext, IHttpContextAccessor httpContextAccessor)

    {

        _dbContext = dbContext;

        _httpContextAccessor = httpContextAccessor;


        // Retrieve the host and port from the HTTP context or environment

        InitializeHostAndPort();

    }


    private void InitializeHostAndPort()

    {

        // Retrieve host and port from the current HTTP context, if available

        if (_httpContextAccessor.HttpContext != null)

        {

            var request = _httpContextAccessor.HttpContext.Request;

            _hostName = request.Host.Host;

            _port = request.Host.Port ?? 80; // Default to 80 if port is not specified

        }

        else

        {

            // Use fallback, such as environment variables or predefined config

            _hostName = Environment.GetEnvironmentVariable("HOSTNAME") ?? "localhost";

            _port = int.TryParse(Environment.GetEnvironmentVariable("PORT"), out int port) ? port : 5000;

        }

    }


    public void Start()

    {

        _timer = new Timer(CheckLeadership, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));

    }


    private void CheckLeadership(object state)

    {

        using (var transaction = _dbContext.Database.BeginTransaction())

        {

            var existingLeader = _dbContext.LeaderElections

                .OrderByDescending(le => le.LastHeartbeat)

                .FirstOrDefault(le => le.IsLeader);


            if (existingLeader != null && (DateTime.UtcNow - existingLeader.LastHeartbeat).TotalSeconds < 10)

            {

                // Existing leader is still alive

                if (existingLeader.HostName == _hostName && existingLeader.Port == _port)

                {

                    // Update own heartbeat

                    existingLeader.LastHeartbeat = DateTime.UtcNow;

                    _dbContext.SaveChanges();

                }

            }

            else

            {

                // No leader or leader has failed, try to become leader

                var self = _dbContext.LeaderElections

                    .FirstOrDefault(le => le.HostName == _hostName && le.Port == _port);


                if (self == null)

                {

                    self = new LeaderElection

                    {

                        HostName = _hostName,

                        Port = _port,

                        IsLeader = true,

                        LastHeartbeat = DateTime.UtcNow

                    };

                    _dbContext.LeaderElections.Add(self);

                }

                else

                {

                    self.IsLeader = true;

                    self.LastHeartbeat = DateTime.UtcNow;

                }


                _dbContext.SaveChanges();

            }


            transaction.Commit();

        }

    }

}


No comments: