<commit_before>using System;
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Orleans.Configuration;
using Orleans.Runtime;
using StackExchange.Redis;

namespace Orleans.GrainDirectory.Redis
{
    public class RedisGrainDirectory : IGrainDirectory, ILifecycleParticipant<ISiloLifecycle>
    {
        private readonly RedisGrainDirectoryOptions directoryOptions;
        private readonly ClusterOptions clusterOptions;
        private readonly ILogger<RedisGrainDirectory> logger;

        private ConnectionMultiplexer redis;
        private IDatabase database;

        public RedisGrainDirectory(
            RedisGrainDirectoryOptions directoryOptions,
            IOptions<ClusterOptions> clusterOptions,
            ILogger<RedisGrainDirectory> logger)
        {
            this.directoryOptions = directoryOptions;
            this.logger = logger;
            this.clusterOptions = clusterOptions.Value;
        }

        public async Task<GrainAddress> Lookup(string grainId)
        {
            try
            {
                var result = (string)await this.database.StringGetAsync(GetKey(grainId));

                if (this.logger.IsEnabled(LogLevel.Debug))
                    this.logger.LogDebug("Lookup {GrainId}: {Result}", grainId, string.IsNullOrWhiteSpace(result) ? "null" : result);

                if (string.IsNullOrWhiteSpace(result))
                    return default;

                return JsonConvert.DeserializeObject<GrainAddress>(result);
            }
            catch (Exception ex)
            {
                this.logger.LogError(ex, "Lookup failed for {GrainId}", grainId);

                if (IsRedisException(ex))
                    throw new OrleansException($"Lookup failed for {grainId} : {ex.ToString()}");
                else
                    throw;
            }
        }

        public async Task<GrainAddress> Register(GrainAddress address)
        {
            var value = JsonConvert.SerializeObject(address);

            try
            {
                var success = await this.database.StringSetAsync(
                    this.GetKey(address.GrainId),
                    value,
                    this.directoryOptions.EntryExpiry,
                    When.NotExists);

                if (this.logger.IsEnabled(LogLevel.Debug))
                    this.logger.LogDebug("Register {GrainId} ({Address}): {Result}", address.GrainId, value, success ? "OK" : "Conflict");

                if (success)
                    return address;

                return await Lookup(address.GrainId);
            }
            catch (Exception ex)
            {
                this.logger.LogError(ex, "Register failed for {GrainId} ({Address})", address.GrainId, value);

                if (IsRedisException(ex))
                    throw new OrleansException($"Register failed for {address.GrainId} ({value}) : {ex.ToString()}");
                else
                    throw;
            }
        }

        public async Task Unregister(GrainAddress address)
        {
            var key = GetKey(address.GrainId);
            var value = JsonConvert.SerializeObject(address);

            try
            {
                var tx = this.database.CreateTransaction();
                tx.AddCondition(Condition.StringEqual(key, value));
                tx.KeyDeleteAsync(key).Ignore();
                var success = await tx.ExecuteAsync();

                if (this.logger.IsEnabled(LogLevel.Debug))
                    this.logger.LogDebug("Unregister {GrainId} ({Address}): {Result}", address.GrainId, value, success ? "OK" : "Conflict");
            }
            catch (Exception ex)
            {
                this.logger.LogError(ex, "Unregister failed for {GrainId} ({Address})", address.GrainId, value);

                if (IsRedisException(ex))
                    throw new OrleansException($"Unregister failed for {address.GrainId} ({value}) : {ex.ToString()}");
                else
                    throw;
            }
        }

        public Task UnregisterSilos(List<string> siloAddresses)
        {
            return Task.CompletedTask;
        }

        public void Participate(ISiloLifecycle lifecycle)
        {
            lifecycle.Subscribe(nameof(RedisGrainDirectory), ServiceLifecycleStage.RuntimeInitialize, Initialize, Uninitialize);
        }

        public async Task Initialize(CancellationToken ct = default)
        {
            this.redis = await ConnectionMultiplexer.ConnectAsync(this.directoryOptions.ConfigurationOptions);

            // Configure logging
            this.redis.ConnectionRestored += this.LogConnectionRestored;
            this.redis.ConnectionFailed += this.LogConnectionFailed;
            this.redis.ErrorMessage += this.LogErrorMessage;
            this.redis.InternalError += this.LogInternalError;
            this.redis.IncludeDetailInExceptions = true;

            this.database = this.redis.GetDatabase();
        }

        private async Task Uninitialize(CancellationToken arg)
        {
            if (this.redis != null && this.redis.IsConnected)
            {
                await this.redis.CloseAsync();
                this.redis.Dispose();
                this.redis = null;
                this.database = null;
            }
        }

        private string GetKey(string grainId) => $"{this.clusterOptions.ClusterId}-{grainId}";

        #region Logging<commit_msg>Use LUA script to unregister activations (#6571)<commit_after>using System;
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Orleans.Configuration;
using Orleans.Runtime;
using StackExchange.Redis;

namespace Orleans.GrainDirectory.Redis
{
    public class RedisGrainDirectory : IGrainDirectory, ILifecycleParticipant<ISiloLifecycle>
    {
        private readonly RedisGrainDirectoryOptions directoryOptions;
        private readonly ClusterOptions clusterOptions;
        private readonly ILogger<RedisGrainDirectory> logger;

        private ConnectionMultiplexer redis;
        private IDatabase database;
        private LuaScript deleteScript;

        public RedisGrainDirectory(
            RedisGrainDirectoryOptions directoryOptions,
            IOptions<ClusterOptions> clusterOptions,
            ILogger<RedisGrainDirectory> logger)
        {
            this.directoryOptions = directoryOptions;
            this.logger = logger;
            this.clusterOptions = clusterOptions.Value;
        }

        public async Task<GrainAddress> Lookup(string grainId)
        {
            try
            {
                var result = (string)await this.database.StringGetAsync(GetKey(grainId));

                if (this.logger.IsEnabled(LogLevel.Debug))
                    this.logger.LogDebug("Lookup {GrainId}: {Result}", grainId, string.IsNullOrWhiteSpace(result) ? "null" : result);

                if (string.IsNullOrWhiteSpace(result))
                    return default;

                return JsonConvert.DeserializeObject<GrainAddress>(result);
            }
            catch (Exception ex)
            {
                this.logger.LogError(ex, "Lookup failed for {GrainId}", grainId);

                if (IsRedisException(ex))
                    throw new OrleansException($"Lookup failed for {grainId} : {ex.ToString()}");
                else
                    throw;
            }
        }

        public async Task<GrainAddress> Register(GrainAddress address)
        {
            var value = JsonConvert.SerializeObject(address);

            try
            {
                var success = await this.database.StringSetAsync(
                    this.GetKey(address.GrainId),
                    value,
                    this.directoryOptions.EntryExpiry,
                    When.NotExists);

                if (this.logger.IsEnabled(LogLevel.Debug))
                    this.logger.LogDebug("Register {GrainId} ({Address}): {Result}", address.GrainId, value, success ? "OK" : "Conflict");

                if (success)
                    return address;

                return await Lookup(address.GrainId);
            }
            catch (Exception ex)
            {
                this.logger.LogError(ex, "Register failed for {GrainId} ({Address})", address.GrainId, value);

                if (IsRedisException(ex))
                    throw new OrleansException($"Register failed for {address.GrainId} ({value}) : {ex.ToString()}");
                else
                    throw;
            }
        }

        public async Task Unregister(GrainAddress address)
        {
            var key = GetKey(address.GrainId);
            var value = JsonConvert.SerializeObject(address);

            try
            {
                var result = (int) await this.database.ScriptEvaluateAsync(this.deleteScript, new { key = GetKey(address.GrainId), val = JsonConvert.SerializeObject(address) });

                if (this.logger.IsEnabled(LogLevel.Debug))
                    this.logger.LogDebug("Unregister {GrainId} ({Address}): {Result}", address.GrainId, value, (result != 0) ? "OK" : "Conflict");
            }
            catch (Exception ex)
            {
                this.logger.LogError(ex, "Unregister failed for {GrainId} ({Address})", address.GrainId, value);

                if (IsRedisException(ex))
                    throw new OrleansException($"Unregister failed for {address.GrainId} ({value}) : {ex.ToString()}");
                else
                    throw;
            }
        }

        public Task UnregisterSilos(List<string> siloAddresses)
        {
            return Task.CompletedTask;
        }

        public void Participate(ISiloLifecycle lifecycle)
        {
            lifecycle.Subscribe(nameof(RedisGrainDirectory), ServiceLifecycleStage.RuntimeInitialize, Initialize, Uninitialize);
        }

        public async Task Initialize(CancellationToken ct = default)
        {
            this.redis = await ConnectionMultiplexer.ConnectAsync(this.directoryOptions.ConfigurationOptions);

            // Configure logging
            this.redis.ConnectionRestored += this.LogConnectionRestored;
            this.redis.ConnectionFailed += this.LogConnectionFailed;
            this.redis.ErrorMessage += this.LogErrorMessage;
            this.redis.InternalError += this.LogInternalError;
            this.redis.IncludeDetailInExceptions = true;

            this.database = this.redis.GetDatabase();

            this.deleteScript = LuaScript.Prepare(
    @"	
local cur = redis.call('GET', @key)	
if cur == @val  then	
  return redis.call('DEL', @key)	
else	
  return 0	
end	
                ");
        }

        private async Task Uninitialize(CancellationToken arg)
        {
            if (this.redis != null && this.redis.IsConnected)
            {
                await this.redis.CloseAsync();
                this.redis.Dispose();
                this.redis = null;
                this.database = null;
            }
        }

        private string GetKey(string grainId) => $"{this.clusterOptions.ClusterId}-{grainId}";

        #region Logging
{'fraction_non_alphanumeric': 0.09155237180047174, 'fraction_numerical': 0.0005241548003843802, 'mean_word_length': 1.9248850281042411, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3104793', 'n_tokens_mistral': 2938, 'n_tokens_neox': 2814, 'n_words': 586}