@@ -10,21 +10,18 @@ namespace Medallion.Threading.Etcd;
1010/// </summary>
1111public sealed partial class EtcdLeaseDistributedLock : IInternalDistributedLock < EtcdLeaseDistributedLockHandle >
1212{
13- private static readonly TimeSpan MinBusyWaitSleepTime = TimeSpan . FromMilliseconds ( 25 ) ,
14- MaxBusyWaitSleepTime = TimeSpan . FromSeconds ( 1 ) ;
15-
16-
1713 private readonly EtcdClientWrapper _etcdClient ;
18-
1914
15+ private readonly ( TimeoutValue duration , TimeoutValue renewalCadence , TimeoutValue minBusyWaitSleepTime ,
16+ TimeoutValue maxBusyWaitSleepTime ) _options ;
2017
21- public EtcdLeaseDistributedLock ( IEtcdClient client , string lockName , int timeToLive = 60 )
18+ public EtcdLeaseDistributedLock ( IEtcdClient client , string lockName , Action < EtcdLeaseOptionsBuilder > ? options = null )
2219 {
2320 this . LockName = lockName ?? throw new ArgumentNullException ( nameof ( lockName ) ) ;
2421 if ( lockName . Length == 0 ) { throw new FormatException ( $ "{ nameof ( lockName ) } : may not have an empty file name") ; }
2522
2623 this . _etcdClient = new EtcdClientWrapper ( client ) ;
27- this . TimeToLive = timeToLive ;
24+ this . _options = EtcdLeaseOptionsBuilder . GetOptions ( options ) ;
2825 }
2926
3027 // todo revisit API
@@ -33,36 +30,169 @@ public EtcdLeaseDistributedLock(IEtcdClient client, string lockName, int timeToL
3330 /// </summary>
3431 public string LockName { get ; }
3532
36- public long TimeToLive { get ; private set ; }
3733
3834 ValueTask < EtcdLeaseDistributedLockHandle ? > IInternalDistributedLock < EtcdLeaseDistributedLockHandle > .
3935 InternalTryAcquireAsync ( TimeoutValue timeout , CancellationToken cancellationToken ) =>
4036 BusyWaitHelper . WaitAsync (
4137 state : this ,
4238 tryGetValue : ( @this , token ) => @this . TryAcquireAsync ( token ) ,
4339 timeout : timeout ,
44- minSleepTime : MinBusyWaitSleepTime ,
45- maxSleepTime : MaxBusyWaitSleepTime ,
40+ minSleepTime : this . _options . minBusyWaitSleepTime ,
41+ maxSleepTime : this . _options . maxBusyWaitSleepTime ,
4642 cancellationToken
4743 ) ;
4844
4945 private async ValueTask < EtcdLeaseDistributedLockHandle ? > TryAcquireAsync ( CancellationToken cancellationToken )
5046 {
5147 cancellationToken . ThrowIfCancellationRequested ( ) ;
5248 // 1 lease per application is enough in my case
53- var leaseResponse = await this . _etcdClient . LeaseGrantAsync ( new LeaseGrantRequest { TTL = this . TimeToLive } ,
49+ var leaseResponse = await this . _etcdClient . LeaseGrantAsync (
50+ new LeaseGrantRequest { TTL = this . _options . renewalCadence . InSeconds } ,
5451 cancellationToken : cancellationToken ) ;
5552 var leaseId = leaseResponse . ID ;
5653 var cancellationTokenSource = new CancellationTokenSource ( ) ;
5754 // this will start a task to renew the lease every 1/3 of the ttl, should i use the ILeaseHandle here instead
5855 _ = this . _etcdClient . LeaseKeepAlive ( leaseId , cancellationTokenSource . Token ) ;
5956 var response =
6057 await this . _etcdClient . LockAsync (
61- new LockRequest { Name = Google . Protobuf . ByteString . CopyFromUtf8 ( this . LockName ) , Lease = leaseId } ,
58+ new LockRequest { Name = Google . Protobuf . ByteString . CopyFromUtf8 ( this . LockName ) , Lease = leaseId , } ,
6259 cancellationToken : cancellationTokenSource . Token ) . ConfigureAwait ( false ) ;
6360 var actualKey = response . Key ;
6461 return new EtcdLeaseDistributedLockHandle ( actualKey . ToString ( ) , this . _etcdClient , leaseId ) ;
6562 }
6663
6764 public string Name => this . LockName ;
65+
66+ public static string GetSafeName ( string name )
67+ {
68+ // TODO figure
69+ return DistributedLockHelpers . ToSafeName ( name , 1000 , s => s ) ;
70+ }
71+ }
72+
73+ /// <summary>
74+ /// basically similar to AzureBlobLeaseDistributedLock
75+ /// </summary>
76+ public class EtcdLeaseOptionsBuilder
77+ {
78+ /// <summary>
79+ /// From https://docs.microsoft.com/en-us/rest/api/storageservices/lease-blob:
80+ /// "The lock duration can be 15 to 60 seconds, or can be infinite"
81+ /// </summary>
82+ internal static readonly TimeoutValue MinLeaseDuration = TimeSpan . FromSeconds ( 15 ) ,
83+ MaxNonInfiniteLeaseDuration = TimeSpan . FromSeconds ( 60 ) ,
84+ DefaultLeaseDuration = TimeSpan . FromSeconds ( 30 ) ;
85+
86+ private TimeoutValue ? _duration , _renewalCadence , _minBusyWaitSleepTime , _maxBusyWaitSleepTime ;
87+
88+ internal EtcdLeaseOptionsBuilder ( ) { }
89+
90+ /// <summary>
91+ /// Specifies how long the lease will last, absent auto-renewal.
92+ ///
93+ /// If auto-renewal is enabled (the default), then a shorter duration means more frequent auto-renewal requests,
94+ /// while an infinite duration means no auto-renewal requests. Furthermore, if the lease-holding process were to
95+ /// exit without explicitly releasing, then duration determines how long other processes would need to wait in
96+ /// order to acquire the lease.
97+ ///
98+ /// If auto-renewal is disabled, then duration determines how long the lease will be held.
99+ ///
100+ /// Defaults to 30s.
101+ /// </summary>
102+ public EtcdLeaseOptionsBuilder Duration ( TimeSpan duration )
103+ {
104+ var durationTimeoutValue = new TimeoutValue ( duration , nameof ( duration ) ) ;
105+ if ( durationTimeoutValue . CompareTo ( MinLeaseDuration ) < 0
106+ || ( ! durationTimeoutValue . IsInfinite && durationTimeoutValue . CompareTo ( MaxNonInfiniteLeaseDuration ) > 0 ) )
107+ {
108+ throw new ArgumentOutOfRangeException ( nameof ( duration ) , duration ,
109+ $ "Must be infinite or in [{ MinLeaseDuration } , { MaxNonInfiniteLeaseDuration } ]") ;
110+ }
111+
112+ this . _duration = durationTimeoutValue ;
113+ return this ;
114+ }
115+
116+ /// <summary>
117+ /// Determines how frequently the lease will be renewed when held. More frequent renewal means more unnecessary requests
118+ /// but also a lower chance of losing the lease due to the process hanging or otherwise failing to get its renewal request in
119+ /// before the lease duration expires.
120+ ///
121+ /// To disable auto-renewal, specify <see cref="Timeout.InfiniteTimeSpan"/>
122+ ///
123+ /// Defaults to 1/3 of the specified lease duration (may be infinite).
124+ /// </summary>
125+ public EtcdLeaseOptionsBuilder RenewalCadence ( TimeSpan renewalCadence )
126+ {
127+ this . _renewalCadence = new TimeoutValue ( renewalCadence , nameof ( renewalCadence ) ) ;
128+ return this ;
129+ }
130+
131+ /// <summary>
132+ /// Waiting to acquire a lease requires a busy wait that alternates acquire attempts and sleeps.
133+ /// This determines how much time is spent sleeping between attempts. Lower values will raise the
134+ /// volume of acquire requests under contention but will also raise the responsiveness (how long
135+ /// it takes a waiter to notice that a contended the lease has become available).
136+ ///
137+ /// Specifying a range of values allows the implementation to select an actual value in the range
138+ /// at random for each sleep. This helps avoid the case where two clients become "synchronized"
139+ /// in such a way that results in one client monopolizing the lease.
140+ ///
141+ /// The default is [250ms, 1s]
142+ /// </summary>
143+ public EtcdLeaseOptionsBuilder BusyWaitSleepTime ( TimeSpan min , TimeSpan max )
144+ {
145+ var minTimeoutValue = new TimeoutValue ( min , nameof ( min ) ) ;
146+ var maxTimeoutValue = new TimeoutValue ( max , nameof ( max ) ) ;
147+
148+ if ( minTimeoutValue . IsInfinite ) { throw new ArgumentOutOfRangeException ( nameof ( min ) , "may not be infinite" ) ; }
149+
150+ if ( maxTimeoutValue . IsInfinite || maxTimeoutValue . CompareTo ( min ) < 0 )
151+ {
152+ throw new ArgumentOutOfRangeException ( nameof ( max ) , max ,
153+ "must be non-infinite and greater than " + nameof ( min ) ) ;
154+ }
155+
156+ this . _minBusyWaitSleepTime = minTimeoutValue ;
157+ this . _maxBusyWaitSleepTime = maxTimeoutValue ;
158+ return this ;
159+ }
160+
161+ internal static ( TimeoutValue duration , TimeoutValue renewalCadence , TimeoutValue minBusyWaitSleepTime , TimeoutValue
162+ maxBusyWaitSleepTime ) GetOptions ( Action < EtcdLeaseOptionsBuilder > ? optionsBuilder )
163+ {
164+ EtcdLeaseOptionsBuilder ? options ;
165+ if ( optionsBuilder != null )
166+ {
167+ options = new EtcdLeaseOptionsBuilder ( ) ;
168+ optionsBuilder ( options ) ;
169+
170+ if ( options . _renewalCadence is { } renewalCadence && ! renewalCadence . IsInfinite )
171+ {
172+ var duration = options . _duration ?? DefaultLeaseDuration ;
173+ if ( renewalCadence . CompareTo ( duration ) >= 0 )
174+ {
175+ throw new ArgumentOutOfRangeException (
176+ nameof ( renewalCadence ) ,
177+ renewalCadence . TimeSpan ,
178+ $ "{ nameof ( renewalCadence ) } must not be larger than { nameof ( duration ) } ({ duration } ). To disable auto-renewal, specify { nameof ( Timeout ) } .{ nameof ( Timeout . InfiniteTimeSpan ) } "
179+ ) ;
180+ }
181+ }
182+ }
183+ else
184+ {
185+ options = null ;
186+ }
187+
188+ var durationToUse = options ? . _duration ?? DefaultLeaseDuration ;
189+ return (
190+ duration : durationToUse ,
191+ renewalCadence : options ? . _renewalCadence ?? ( durationToUse . IsInfinite
192+ ? Timeout . InfiniteTimeSpan
193+ : TimeSpan . FromMilliseconds ( durationToUse . InMilliseconds / 3.0 ) ) ,
194+ minBusyWaitSleepTime : options ? . _minBusyWaitSleepTime ?? TimeSpan . FromMilliseconds ( 250 ) ,
195+ maxBusyWaitSleepTime : options ? . _maxBusyWaitSleepTime ?? TimeSpan . FromSeconds ( 1 )
196+ ) ;
197+ }
68198}
0 commit comments