public RedisDistributedLock(IOptions <DistributedLockOptions> options, IServiceProvider serviceProvider) { var multiplexers = options.Value.RedisEndPoints .Select(x => new RedLockMultiplexer(ConnectionMultiplexer.Connect(x))) .ToList(); _redLockFactory = RedLockFactory.Create(multiplexers); IHostApplicationLifetime hostApplicationLifetime = serviceProvider.GetService <IHostApplicationLifetime>(); if (hostApplicationLifetime != null) { hostApplicationLifetime.ApplicationStopping.Register(() => { _redLockFactory.Dispose(); }); } }
public async Task TestBlockingConcurrentNestingLocks() { using (var redisLockFactory = RedLockFactory.Create(AllActiveEndPoints, loggerFactory)) { var resource = $"testredislock:{Guid.NewGuid()}"; using (var firstLock = await redisLockFactory.CreateLockAsync(resource, TimeSpan.FromSeconds(1))) { Assert.That(firstLock.IsAcquired, Is.True); using (var secondLock = await redisLockFactory.CreateLockAsync(resource, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(20), TimeSpan.FromSeconds(1))) { Assert.That(secondLock.IsAcquired, Is.True); Assert.That(secondLock.Status, Is.EqualTo(RedLockStatus.Acquired)); } } } }
/// <summary> /// Create instance of RedLock factory /// </summary> /// <returns>RedLock factory</returns> protected RedLockFactory CreateRedisLockFactory() { //get RedLock endpoints var configurationOptions = ConfigurationOptions.Parse(_connectionString.Value); var redLockEndPoints = GetEndPoints().Select(endPoint => new RedLockEndPoint { EndPoint = endPoint, Password = configurationOptions.Password, Ssl = configurationOptions.Ssl, RedisDatabase = configurationOptions.DefaultDatabase, ConfigCheckSeconds = configurationOptions.ConfigCheckSeconds, ConnectionTimeout = configurationOptions.ConnectTimeout, SyncTimeout = configurationOptions.SyncTimeout }).ToList(); //create RedLock factory to use RedLock distributed lock algorithm return(RedLockFactory.Create(redLockEndPoints)); }
private async Task DoOverlappingLocksAsync() { using (var redisLockFactory = RedLockFactory.Create(AllActiveEndPoints, loggerFactory)) { var resource = $"testredislock:{Guid.NewGuid()}"; using (var firstLock = await redisLockFactory.CreateLockAsync(resource, TimeSpan.FromSeconds(30))) { Assert.That(firstLock.IsAcquired, Is.True); using (var secondLock = await redisLockFactory.CreateLockAsync(resource, TimeSpan.FromSeconds(30))) { Assert.That(secondLock.IsAcquired, Is.False); Assert.That(secondLock.Status, Is.EqualTo(RedLockStatus.Conflicted)); } } } }
public void TestBlockingConcurrentLocks() { var locksAcquired = 0; using (var redisLockFactory = RedLockFactory.Create(AllActiveEndPoints)) { var resource = $"testblockingconcurrentlocks:{Guid.NewGuid()}"; var threads = new List <Thread>(); for (var i = 0; i < 2; i++) { var thread = new Thread(() => { // ReSharper disable once AccessToDisposedClosure (we join on threads before disposing) using (var redisLock = redisLockFactory.CreateLock( resource, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(0.5))) { logger.Info("Entering lock"); if (redisLock.IsAcquired) { Interlocked.Increment(ref locksAcquired); } Thread.Sleep(4000); logger.Info("Leaving lock"); } }); thread.Start(); threads.Add(thread); } foreach (var thread in threads) { thread.Join(); } } Assert.That(locksAcquired, Is.EqualTo(2)); }
public async Task TimeLock() { using (var redisLockFactory = RedLockFactory.Create(AllActiveEndPoints, loggerFactory)) { var resource = $"testredislock:{Guid.NewGuid()}"; // warmup for (var i = 0; i < 10; i++) { using (await redisLockFactory.CreateLockAsync(resource, TimeSpan.FromSeconds(30))) { } } var sw = new Stopwatch(); var totalAcquire = new TimeSpan(); var totalRelease = new TimeSpan(); var iterations = 10000; for (var i = 0; i < iterations; i++) { sw.Restart(); using (var redisLock = await redisLockFactory.CreateLockAsync(resource, TimeSpan.FromSeconds(30))) { sw.Stop(); Assert.That(redisLock.IsAcquired, Is.True); logger.LogInformation($"Acquire {i} took {sw.ElapsedTicks} ticks, status: {redisLock.Status}"); totalAcquire += sw.Elapsed; sw.Restart(); } sw.Stop(); logger.LogInformation($"Release {i} took {sw.ElapsedTicks} ticks, success"); totalRelease += sw.Elapsed; } logger.LogWarning($"{iterations} iterations, total acquire time: {totalAcquire}, total release time {totalRelease}"); } }
public void TestQuorum() { logger.LogInformation("======== Testing quorum with all active endpoints ========"); CheckSingleRedisLock( () => RedLockFactory.Create(AllActiveEndPoints, loggerFactory), RedLockStatus.Acquired); logger.LogInformation("======== Testing quorum with no active endpoints ========"); CheckSingleRedisLock( () => RedLockFactory.Create(AllInactiveEndPoints, loggerFactory), RedLockStatus.NoQuorum); logger.LogInformation("======== Testing quorum with enough active endpoints ========"); CheckSingleRedisLock( () => RedLockFactory.Create(SomeActiveEndPointsWithQuorum, loggerFactory), RedLockStatus.Acquired); logger.LogInformation("======== Testing quorum with not enough active endpoints ========"); CheckSingleRedisLock( () => RedLockFactory.Create(SomeActiveEndPointsWithNoQuorum, loggerFactory), RedLockStatus.NoQuorum); }
public static void DoIt() { var redlockFactory = RedLockFactory.Create(new List <RedLockEndPoint> { new DnsEndPoint("localhost", 6379) }); string resource = "some-resource"; using (var firstLock = redlockFactory.CreateLock(resource, TimeSpan.FromSeconds(0.1))) { PrintLock(firstLock); using (var secondLock = redlockFactory.CreateLock(resource, TimeSpan.FromSeconds(0.1), TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(0.25))) { PrintLock(secondLock); } } }
public void TestQuorum() { logger.Info("======== Testing quorum with all active endpoints ========"); CheckSingleRedisLock( () => RedLockFactory.Create(AllActiveEndPoints), true); logger.Info("======== Testing quorum with no active endpoints ========"); CheckSingleRedisLock( () => RedLockFactory.Create(AllInactiveEndPoints), false); logger.Info("======== Testing quorum with enough active endpoints ========"); CheckSingleRedisLock( () => RedLockFactory.Create(SomeActiveEndPointsWithQuorum), true); logger.Info("======== Testing quorum with not enough active endpoints ========"); CheckSingleRedisLock( () => RedLockFactory.Create(SomeActiveEndPointsWithNoQuorum), false); }
public async Task <bool> TryAddAsync(string key) { if (await _database.KeyExistsAsync((RedisKey)key)) { return(false); } using (var factory = RedLockFactory.Create(new[] { new RedLockMultiplexer(_database.Multiplexer) })) using (var redLock = factory.CreateLock(key, TimeSpan.FromMinutes(1))) { if (!redLock.IsAcquired || await _database.KeyExistsAsync((RedisKey)key)) { return(false); } var value = _serializer.Serialize(IdempotencyRegister.Of(key)); return(await _database.StringSetAsync(key, value, TimeSpan.FromMinutes(1), When.NotExists)); } }
public static IServiceCollection AddDistributeLock(this IServiceCollection services, Action <DistributeLockOption> redisConfigure) { DistributeLockOption distributeLockOption = new DistributeLockOption() { RedisConnectionStrings = new List <string>() }; redisConfigure.Invoke(distributeLockOption); var multiplexers = new List <RedLockMultiplexer>(); foreach (var conn in distributeLockOption.RedisConnectionStrings) { multiplexers.Add(ConnectionMultiplexer.Connect(conn)); } var redlockFactory = RedLockFactory.Create(multiplexers); services.AddSingleton(redlockFactory); services.AddSingleton <ILockFactory, DistributeLockFactory>(); return(services); }
public void TestRenewing() { using (var redisLockFactory = RedLockFactory.Create(AllActiveEndPoints)) { var resource = $"testrenewinglock:{Guid.NewGuid()}"; int extendCount; using (var redisLock = redisLockFactory.CreateLock(resource, TimeSpan.FromSeconds(2))) { Assert.That(redisLock.IsAcquired, Is.True); Thread.Sleep(4000); extendCount = redisLock.ExtendCount; } Assert.That(extendCount, Is.GreaterThan(2)); } }
static void Main(string[] args) { var endPoint = new List <RedLockEndPoint> { new DnsEndPoint("192.168.233.128", 6379), }; var redLockFactory = RedLockFactory.Create(endPoint); Task.Factory.StartNew(() => { using (var redLock = redLockFactory.CreateLock(resource, expiry)) { if (redLock.IsAcquired) { Console.WriteLine("task 获取到锁"); Thread.Sleep(10000); } else { Console.WriteLine("task 锁被占用"); } } }); Thread.Sleep(1000); using (var redLock = redLockFactory.CreateLock(resource, expiry, wait, retry)) { if (redLock.IsAcquired) { Console.WriteLine("main 获取到锁"); } else { Console.WriteLine("main 锁被占用"); } } Console.Read(); }
public void TestLockReleasedAfterTimeout() { using (var lockFactory = RedLockFactory.Create(AllActiveEndPoints)) { var resource = $"testrenewinglock:{Guid.NewGuid()}"; using (var firstLock = lockFactory.CreateLock(resource, TimeSpan.FromSeconds(1))) { Assert.That(firstLock.IsAcquired, Is.True); Thread.Sleep(550); // should cause keep alive timer to fire once ((RedLock)firstLock).StopKeepAliveTimer(); // stop the keep alive timer to simulate process crash Thread.Sleep(1200); // wait until the key expires from redis using (var secondLock = lockFactory.CreateLock(resource, TimeSpan.FromSeconds(1))) { Assert.That(secondLock.IsAcquired, Is.True); // Eventually the outer lock should timeout } } } }
static void Main(string[] args) { ConnectionMultiplexer connection = ConnectionMultiplexer.Connect("127.0.0.0:6379,allowAdmin=true"); var redLockFactory = new RedLockFactory(connection); using (var redLock = redLockFactory.GetLock("lock", TimeSpan.FromMinutes(10), 2, TimeSpan.FromSeconds(2))) { Console.WriteLine("[A] Try to get lock"); if (redLock.IsAcquired) { Console.WriteLine("[A] Geted lock"); Thread.Sleep(10000); } else { Console.WriteLine("[A] UnAcquired"); } } Console.WriteLine("[A] End"); Console.ReadLine(); }
public static IServiceCollection AddRedis(this IServiceCollection services) { services.AddSingleton <IConnectionMultiplexer>(x => { var c = x.GetRequiredService <IConfiguration>(); var config = c["ConnectionStrings:CacheConnection"]; return(ConnectionMultiplexer.Connect(config)); }); services.AddScoped(x => x.GetRequiredService <IConnectionMultiplexer>().GetDatabase()); services.AddSingleton <IDistributedLockFactory>(x => { var conn = x.GetRequiredService <IConnectionMultiplexer>(); return(RedLockFactory.Create(new List <RedLockMultiplexer> { new RedLockMultiplexer(conn) })); }); services.AddSingleton <IResourceLockerFactory, ResourceLockerFactory>(); services.AddScoped <InRedisComicService>(); return(services); }
static RedLockFactory RedisConnection() { //自行管理連線 //var endPoints = new List<RedLockEndPoint> //{ // new DnsEndPoint("redis1",6379), //}; //var redlockFactory = RedLockFactory.Create(endPoints); //公用連線 var existingConnectionMultiplexer1 = ConnectionMultiplexer.Connect("localhost:6379"); var multiplexers = new List <RedLockMultiplexer> { existingConnectionMultiplexer1, }; var redlockFactory = RedLockFactory.Create(multiplexers); return(redlockFactory); }
/// <summary> /// Run specified payload in sync between several instances /// </summary> /// <param name="app"></param> /// <param name="payload"></param> /// <returns></returns> public static IApplicationBuilder WithDistributedLock(this IApplicationBuilder app, Action payload) { var redisConnMultiplexer = app.ApplicationServices.GetService <IConnectionMultiplexer>(); var logger = app.ApplicationServices.GetService <ILogger <Startup> >(); if (redisConnMultiplexer != null) { var distributedLockWait = app.ApplicationServices.GetRequiredService <IOptions <LocalStorageModuleCatalogOptions> >().Value.DistributedLockWait; // Try to acquire distributed lock using (var redlockFactory = RedLockFactory.Create(new RedLockMultiplexer[] { new RedLockMultiplexer(redisConnMultiplexer) })) using (var redLock = redlockFactory.CreateLock(nameof(WithDistributedLock), TimeSpan.FromSeconds(120 + distributedLockWait) /* Successfully acquired lock expiration time */, TimeSpan.FromSeconds(distributedLockWait) /* Total time to wait until the lock is available */, TimeSpan.FromSeconds(3) /* The span to acquire the lock in retries */)) { if (redLock.IsAcquired) { logger.LogInformation("Distributed lock acquired"); payload(); } else { // Lock not acquired even after migrationDistributedLockOptions.Wait throw new PlatformException($"Can't apply migrations. It seems another platform instance still applies migrations. Consider to increase MigrationDistributedLockOptions.Wait timeout."); } } } else { // One-instance configuration, no Redis, just run logger.Log(LogLevel.Information, "Distributed lock not acquired, Redis ConnectionMultiplexer is null (No Redis connection ?)"); payload(); } return(app); }
private static void ConfigureDependencies() { Bootstrap.Initialize(); var connString = new MongoUrl(Environment.GetEnvironmentVariable("MONGO_CONNECTION")); DbFactory.SetClient(new MongoClient(connString)); var dbFactory = new DbFactory(); var settingsCollection = dbFactory.GetCollection <Common.Model.Setting <Common.Model.Redis.Root> >(CollectionNames.Settings); var redisSettings = settingsCollection.AsQueryable().Where(s => s.Key == "redisConnection").First().Value; var endPoints = new List <RedLockEndPoint>(); foreach (var endPoint in redisSettings.Hosts.Select(h => new DnsEndPoint(h.Address, h.Port)).ToList()) { endPoints.Add(endPoint); } var redLockFactory = RedLockFactory.Create(endPoints); Monitor.Initialize(new ConnectionFactory(), new DbFactory(), redLockFactory); }
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { //services.AddHostedService<MyRabbitMQConsumerHostService>(); services.AddDbContext <MyDbContext>(builder => { builder.UseMySql("server=localhost;uid=root;pwd=root;database=shop;"); }); services.AddScoped <ISaleService, SaleService>(); services.AddScoped <IProductService, ProductService>(); services.AddScoped <IOrderService, OrderService>(); var lockFactory = RedLockFactory.Create(new List <RedLockEndPoint>() { new DnsEndPoint("127.0.0.1", 6379) }); services.AddSingleton(typeof(IDistributedLockFactory), lockFactory); services.AddSingleton <RabbitMQConnectionService>(); services.AddControllers(); }
protected RedLockFactory CreateRedisLockFactory() { //get RedLock endpoints configurations var configurationOptions = ConfigurationOptions.Parse(MonacoConfiguration.Instance.RedLockConfig.EndPoints); //get RedLock endpoints List <RedLockEndPoint> redLockEndPoints = new List <RedLockEndPoint>(); foreach (var endpoint in configurationOptions.EndPoints) { redLockEndPoints.Add(new RedLockEndPoint { EndPoint = endpoint, Password = configurationOptions.Password, Ssl = configurationOptions.Ssl, RedisDatabase = configurationOptions.DefaultDatabase, ConfigCheckSeconds = configurationOptions.ConfigCheckSeconds, ConnectionTimeout = configurationOptions.ConnectTimeout, SyncTimeout = configurationOptions.SyncTimeout }); } //create RedLock factory to use RedLock distributed lock algorithm return(RedLockFactory.Create(redLockEndPoints)); }
/// <summary> /// Run payload method with distributed lock /// </summary> /// <param name="resourceId">Identifier of locking resource</param> /// <param name="payload">Payload method to run under the acquired lock</param> public virtual void ExecuteSynhronized(string resourceId, Action <DistributedLockCondition> payload) { using (var redlockFactory = RedLockFactory.Create(new RedLockMultiplexer[] { new RedLockMultiplexer(_redisConnMultiplexer) })) { var instantlyAcquired = false; var expiryTime = 120 + _waitTime; // Try to acquire distributed lock and giving up immediately if the lock is not available using (var redLock = redlockFactory.CreateLock(resourceId, TimeSpan.FromSeconds(expiryTime) /* Successfully acquired lock expiration time */)) { if (redLock.IsAcquired) { instantlyAcquired = true; _logger.LogInformation(@$ "Distributed lock: run payload for resource {resourceId} instantly."); payload(DistributedLockCondition.Instant); } } if (!instantlyAcquired) { // Try to acquire distributed lock with awaiting using (var redLock = redlockFactory.CreateLock(resourceId, TimeSpan.FromSeconds(expiryTime) /* Successfully acquired lock expiration time */, TimeSpan.FromSeconds(_waitTime) /* Total time to wait until the lock is available */, TimeSpan.FromSeconds(3) /* The span to acquire the lock in retries */)) { if (!redLock.IsAcquired) { // Lock not acquired even after migrationDistributedLockOptions.Wait throw new PlatformException($"Can't acquire distributed lock for resource {this}. It seems that another Platform instance still has the lock, consider increasing wait timeout."); } _logger.LogInformation(@$ "Distributed lock: run payload for resource {resourceId} after awaiting for previous lock."); payload(DistributedLockCondition.Delayed); } } } }
public static RedLockFactory Get(CacheSettings cacheSettings) { try { if (RedLockFactory == null) { lock (Lock) { if (RedLockFactory == null) { var endpoint = new RedLockEndPoint { EndPoint = new DnsEndPoint(cacheSettings.Host, cacheSettings.Port), Password = cacheSettings.Password, Ssl = cacheSettings.Ssl, RedisDatabase = cacheSettings.LockerDb, SyncTimeout = cacheSettings.TimeoutInMs, ConnectionTimeout = cacheSettings.TimeoutInMs, }; var hosts = new List <RedLockEndPoint> { endpoint }; RedLockFactory = RedLockFactory.Create(hosts); } } } } catch (Exception) { CloseConnection(); throw; } return(RedLockFactory); }
private static IServiceCollection AddRedLockFactory(this IServiceCollection services) => services.AddSingleton <IDistributedLockFactory, RedLockFactory>(sp => RedLockFactory.Create( new[] { new RedLockMultiplexer(sp.GetRequiredService <IConnectionMultiplexer>()) }));
public RedisConnectionWrapper(NopConfig config) { _config = config; _connectionString = new Lazy <string>(GetConnectionString); _redisLockFactory = CreateRedisLockFactory(); }
public RedisConnectionWrapper(string connectionString) { _connectionString = new Lazy <string>(connectionString); _redisLockFactory = CreateRedisLockFactory(); }
/// <summary> /// Initializes a new instance of the <see cref="Monitor"/> class. /// </summary> /// <param name="connectionFactory">The RabbitMQ connection factory</param> /// <param name="dbFactory">The database factory</param> /// <param name="redLockFactory">The redis lock factory</param> public static void Initialize(IConnectionFactory connectionFactory, IDbFactory dbFactory, RedLockFactory redLockFactory) { Monitor.connectionFactory = connectionFactory; Monitor.dbFactory = dbFactory; Monitor.redLockFactory = redLockFactory; }
/// <summary> /// Ctor /// </summary> /// <param name="config">Config</param> public RedisConnectionWrapper(GameConfig config) { this._config = config; this._connectionString = new Lazy <string>(GetConnectionString); this._redisLockFactory = CreateRedisLockFactory(); }
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); var DataBaseSection = Configuration.GetSection("ConnectionStrings:DefaultConnection").Value; var RedisSection = Configuration.GetSection("ConnectionStrings:RedisConnection").Value; var RabbitMQHost = Configuration.GetSection("RabbitMQ:Host").Value; var RabbitMQPort = ushort.Parse(Configuration.GetSection("RabbitMQ:Port").Value); var RabbitMQVhost = Configuration.GetSection("RabbitMQ:Vhost").Value; var RabbitMQUser = Configuration.GetSection("RabbitMQ:User").Value; var RabbitMQPassword = Configuration.GetSection("RabbitMQ:Password").Value; //#if DEBUG // var DataBaseSection = Configuration.GetSection("ConnectionStrings:DefaultConnection").Value; // var RedisSection = Configuration.GetSection("ConnectionStrings:RedisConnection").Value; // var RabbitMQHost = Configuration.GetSection("RabbitMQ:Host").Value; // var RabbitMQPort = ushort.Parse(Configuration.GetSection("RabbitMQ:Port").Value); // var RabbitMQVhost = Configuration.GetSection("RabbitMQ:Vhost").Value; // var RabbitMQUser = Configuration.GetSection("RabbitMQ:User").Value; // var RabbitMQPassword = Configuration.GetSection("RabbitMQ:Password").Value; //#else // var Hostname = Configuration["SQLSERVER_HOST"]; // var User = Configuration["SQLSERVER_USER"]; // var Password = Configuration["SQLSERVER_PASSWORD"]; // var Catalog = Configuration["SQLSERVER_CATALOG"]; // var DataBaseSection = $"Data Source={Hostname};Initial Catalog={Catalog};User ID={User};Password={Password};"; // var RedisSection = Configuration["REDIS_HOST"]; // var RabbitMQHost = Configuration["RABBITMQ_HOST"]; // var RabbitMQPort = ushort.Parse(Configuration["RABBITMQ_PORT"]); // var RabbitMQVhost = Configuration["RABBITMQ_VHOST"]; // var RabbitMQUser = Configuration["RABBITMQ_USER"]; // var RabbitMQPassword = Configuration["RABBITMQ_PASSWORD"]; //#endif services.AddScoped <ICheckingAccountTransactionRepository, CheckingAccountTransactionRepository>(); services.AddScoped <ICheckingAccountTransactionStatusRepository, CheckingAccountTransactionStatusRepository>(); services.AddScoped <ICheckingAccountTransactionTypeRepository, CheckingAccountTransactionTypeRepository>(); services.AddScoped <ICurrencyRepository, CurrencyRepository>(); services.AddScoped <ICheckingAccountRepository, CheckingAccountRepository>(); services.AddScoped <ICheckingAccountTransactionDomain, CheckingAccountTransactionDomain>(); services.AddScoped <ICheckingAccountDomain, CheckingAccountDomain>(); services.AddSingleton <IPublishEndpoint>(provider => provider.GetRequiredService <IBusControl>()); services.AddSingleton <ISendEndpointProvider>(provider => provider.GetRequiredService <IBusControl>()); services.AddSingleton <IBus>(provider => provider.GetRequiredService <IBusControl>()); services.AddSingleton <IHostedService, MassTransitHostedService>(); var cm = ConnectionMultiplexer.Connect("10.1.10.133:6378"); services.AddSingleton <IConnectionMultiplexer>(cm); services.AddSingleton <IDistributedLockFactory>(RedLockFactory.Create(new List <RedLockMultiplexer> { cm })); services.AddDbContextPool <DataContext>(options => options.UseSqlServer(DataBaseSection, b => b.MigrationsAssembly("NB.CheckingAccountTransaction.API")) .RalmsExtendFunctions() ); services.AddMassTransit(x => { // add the consumer, for LoadFrom x.AddConsumer <AddInternalTransaction>(); x.AddConsumer <AddCreditCardTransaction>(); x.AddConsumer <CheckingAccountCreated>(); }); services.AddSingleton(provider => Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(RabbitMQHost, RabbitMQPort, RabbitMQVhost, h => { h.Username(RabbitMQUser); h.Password(RabbitMQPassword); }); cfg.ReceiveEndpoint(host, "CheckingAccount_Transactione_Queue", e => { //e.PrefetchCount = 1000; e.LoadFrom(provider); }); })); }
public RedisConnectionWrapper(AppConfig config) { _connectionString = new Lazy <string>(config.RedisCachingConnectionString); _redisLockFactory = CreateRedisLockFactory(); }