counting_semaphore 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,517 @@
1
+ # typed: strong
2
+ # A counting semaphore that allows up to N concurrent operations.
3
+ # When capacity is exceeded, operations block until resources become available.
4
+ # API compatible with concurrent-ruby's Semaphore class.
5
+ module CountingSemaphore
6
+ VERSION = T.let("0.2.0", T.untyped)
7
+
8
+ # Represents an acquired lease on a semaphore.
9
+ # Must be passed to release() to return the permits.
10
+ class Lease < Struct
11
+ # Returns a human-readable representation of the lease
12
+ sig { returns(String) }
13
+ def to_s; end
14
+
15
+ # Returns detailed inspection string
16
+ sig { returns(String) }
17
+ def inspect; end
18
+
19
+ # Returns the value of attribute semaphore
20
+ sig { returns(Object) }
21
+ attr_accessor :semaphore
22
+
23
+ # Returns the value of attribute id
24
+ sig { returns(Object) }
25
+ attr_accessor :id
26
+
27
+ # Returns the value of attribute permits
28
+ sig { returns(Object) }
29
+ attr_accessor :permits
30
+ end
31
+
32
+ # Custom exception raised when a semaphore lease cannot be acquired within the specified timeout.
33
+ # Contains information about the failed acquisition attempt including the semaphore instance,
34
+ # number of permits requested, and the timeout duration.
35
+ class LeaseTimeout < StandardError
36
+ # Creates a new LeaseTimeout exception.
37
+ #
38
+ # _@param_ `permit_count` — Number of permits that were requested
39
+ #
40
+ # _@param_ `timeout_seconds` — The timeout duration that was exceeded
41
+ #
42
+ # _@param_ `semaphore` — The semaphore instance (optional)
43
+ sig { params(permit_count: Integer, timeout_seconds: Numeric, semaphore: T.nilable(T.any(CountingSemaphore::LocalSemaphore, CountingSemaphore::RedisSemaphore))).void }
44
+ def initialize(permit_count, timeout_seconds, semaphore = nil); end
45
+
46
+ # _@return_ — The semaphore that timed out
47
+ #
48
+ # _@return_ — The number of permits that were requested
49
+ #
50
+ # _@return_ — The timeout duration in seconds
51
+ sig { returns(T.nilable(T.any(CountingSemaphore::LocalSemaphore, CountingSemaphore::RedisSemaphore, Integer, Numeric))) }
52
+ attr_reader :semaphore
53
+
54
+ # _@return_ — The semaphore that timed out
55
+ #
56
+ # _@return_ — The number of permits that were requested
57
+ #
58
+ # _@return_ — The timeout duration in seconds
59
+ sig { returns(T.nilable(T.any(CountingSemaphore::LocalSemaphore, CountingSemaphore::RedisSemaphore, Integer, Numeric))) }
60
+ attr_reader :permit_count
61
+
62
+ # _@return_ — The semaphore that timed out
63
+ #
64
+ # _@return_ — The number of permits that were requested
65
+ #
66
+ # _@return_ — The timeout duration in seconds
67
+ sig { returns(T.nilable(T.any(CountingSemaphore::LocalSemaphore, CountingSemaphore::RedisSemaphore, Integer, Numeric))) }
68
+ attr_reader :timeout_seconds
69
+ end
70
+
71
+ # A null logger that discards all log messages.
72
+ # Provides the same interface as a real logger but does nothing.
73
+ # Only yields blocks when ENV["RUN_ALL_LOGGER_BLOCKS"] is set to "yes",
74
+ # which is useful in testing. Block form for Logger calls allows you
75
+ # to skip block evaluation if the Logger level is higher than your
76
+ # call, and thus bugs can nest in those Logger blocks. During
77
+ # testing it is helpful to excercise those blocks unconditionally.
78
+ module NullLogger
79
+ extend CountingSemaphore::NullLogger
80
+
81
+ # Logs a debug message. Discards the message but may yield the block for testing.
82
+ #
83
+ # _@param_ `message` — Optional message to log (discarded)
84
+ sig { params(message: T.nilable(String), block: T.untyped).void }
85
+ def debug(message = nil, &block); end
86
+
87
+ # Logs an info message. Discards the message but may yield the block for testing.
88
+ #
89
+ # _@param_ `message` — Optional message to log (discarded)
90
+ sig { params(message: T.nilable(String), block: T.untyped).void }
91
+ def info(message = nil, &block); end
92
+
93
+ # Logs a warning message. Discards the message but may yield the block for testing.
94
+ #
95
+ # _@param_ `message` — Optional message to log (discarded)
96
+ sig { params(message: T.nilable(String), block: T.untyped).void }
97
+ def warn(message = nil, &block); end
98
+
99
+ # Logs an error message. Discards the message but may yield the block for testing.
100
+ #
101
+ # _@param_ `message` — Optional message to log (discarded)
102
+ sig { params(message: T.nilable(String), block: T.untyped).void }
103
+ def error(message = nil, &block); end
104
+
105
+ # Logs a fatal message. Discards the message but may yield the block for testing.
106
+ #
107
+ # _@param_ `message` — Optional message to log (discarded)
108
+ sig { params(message: T.nilable(String), block: T.untyped).void }
109
+ def fatal(message = nil, &block); end
110
+
111
+ # Logs a debug message. Discards the message but may yield the block for testing.
112
+ #
113
+ # _@param_ `message` — Optional message to log (discarded)
114
+ sig { params(message: T.nilable(String), block: T.untyped).void }
115
+ def self.debug(message = nil, &block); end
116
+
117
+ # Logs an info message. Discards the message but may yield the block for testing.
118
+ #
119
+ # _@param_ `message` — Optional message to log (discarded)
120
+ sig { params(message: T.nilable(String), block: T.untyped).void }
121
+ def self.info(message = nil, &block); end
122
+
123
+ # Logs a warning message. Discards the message but may yield the block for testing.
124
+ #
125
+ # _@param_ `message` — Optional message to log (discarded)
126
+ sig { params(message: T.nilable(String), block: T.untyped).void }
127
+ def self.warn(message = nil, &block); end
128
+
129
+ # Logs an error message. Discards the message but may yield the block for testing.
130
+ #
131
+ # _@param_ `message` — Optional message to log (discarded)
132
+ sig { params(message: T.nilable(String), block: T.untyped).void }
133
+ def self.error(message = nil, &block); end
134
+
135
+ # Logs a fatal message. Discards the message but may yield the block for testing.
136
+ #
137
+ # _@param_ `message` — Optional message to log (discarded)
138
+ sig { params(message: T.nilable(String), block: T.untyped).void }
139
+ def self.fatal(message = nil, &block); end
140
+ end
141
+
142
+ class LocalSemaphore
143
+ include CountingSemaphore::WithLeaseSupport
144
+ SLEEP_WAIT_SECONDS = T.let(0.25, T.untyped)
145
+
146
+ # Initialize the semaphore with a maximum capacity.
147
+ #
148
+ # _@param_ `capacity` — Maximum number of concurrent operations allowed (also called permits)
149
+ #
150
+ # _@param_ `logger` — the logger
151
+ sig { params(capacity: Integer, logger: Logger).void }
152
+ def initialize(capacity, logger: CountingSemaphore::NullLogger); end
153
+
154
+ # Acquires the given number of permits from this semaphore, blocking until all are available.
155
+ #
156
+ # _@param_ `permits` — Number of permits to acquire (default: 1)
157
+ #
158
+ # _@return_ — A lease object that must be passed to release()
159
+ sig { params(permits: Integer).returns(CountingSemaphore::Lease) }
160
+ def acquire(permits = 1); end
161
+
162
+ # Releases a previously acquired lease, returning the permits to the semaphore.
163
+ #
164
+ # _@param_ `lease` — The lease object returned by acquire() or try_acquire()
165
+ sig { params(lease: CountingSemaphore::Lease).void }
166
+ def release(lease); end
167
+
168
+ # Acquires the given number of permits from this semaphore, only if all are available
169
+ # at the time of invocation or within the timeout interval.
170
+ #
171
+ # _@param_ `permits` — Number of permits to acquire (default: 1)
172
+ #
173
+ # _@param_ `timeout` — Number of seconds to wait, or nil to return immediately (default: nil)
174
+ #
175
+ # _@return_ — A lease object if successful, nil otherwise
176
+ sig { params(permits: Integer, timeout: T.nilable(Numeric)).returns(T.nilable(CountingSemaphore::Lease)) }
177
+ def try_acquire(permits = 1, timeout: nil); end
178
+
179
+ # Returns the current number of permits available in this semaphore.
180
+ #
181
+ # _@return_ — Number of available permits
182
+ sig { returns(Integer) }
183
+ def available_permits; end
184
+
185
+ # Acquires and returns all permits that are immediately available.
186
+ # Returns a single lease representing all drained permits.
187
+ #
188
+ # _@return_ — A lease for all available permits, or nil if none available
189
+ sig { returns(T.nilable(CountingSemaphore::Lease)) }
190
+ def drain_permits; end
191
+
192
+ # Acquire a lease for the specified number of permits and execute the block.
193
+ # Blocks until sufficient resources are available.
194
+ # Kept for backwards compatibility - wraps acquire/release.
195
+ #
196
+ # _@param_ `permit_count` — Number of permits to acquire (default: 1)
197
+ #
198
+ # _@param_ `timeout` — Maximum time in seconds to wait for lease acquisition (default: 30). For Redis-backed semaphores, the timeout value will be rounded up to the nearest whole second due to Redis BLPOP limitations.
199
+ #
200
+ # _@return_ — The result of the block
201
+ sig { params(permit_count: Integer, timeout: Numeric, blk: T.proc.params(lease: T.nilable(CountingSemaphore::Lease)).void).returns(T.untyped) }
202
+ def with_lease(permit_count = 1, timeout: 30, &blk); end
203
+
204
+ # Get the current number of permits currently acquired.
205
+ # Kept for backwards compatibility.
206
+ #
207
+ # _@return_ — Number of permits currently in use
208
+ sig { returns(Integer) }
209
+ def currently_leased; end
210
+
211
+ sig { returns(Integer) }
212
+ attr_reader :capacity
213
+ end
214
+
215
+ class RedisSemaphore
216
+ include CountingSemaphore::WithLeaseSupport
217
+ LEASE_EXPIRATION_SECONDS = T.let(5, T.untyped)
218
+ GET_LEASE_SCRIPT = T.let(<<~LUA, T.untyped)
219
+ local lease_key = KEYS[1]
220
+ local lease_set_key = KEYS[2]
221
+ local capacity = tonumber(ARGV[1])
222
+ local permit_count = tonumber(ARGV[2])
223
+ local expiration_seconds = tonumber(ARGV[3])
224
+
225
+ -- Get all active leases from the set and calculate current usage
226
+ local lease_keys = redis.call('SMEMBERS', lease_set_key)
227
+ local current_usage = 0
228
+ local valid_leases = {}
229
+
230
+ for i, key in ipairs(lease_keys) do
231
+ local permits = redis.call('GET', key)
232
+ if permits then
233
+ local permits_from_lease = tonumber(permits)
234
+ if permits_from_lease then
235
+ current_usage = current_usage + permits_from_lease
236
+ table.insert(valid_leases, key)
237
+ else
238
+ -- Remove lease with invalid permit count
239
+ redis.call('DEL', key)
240
+ redis.call('SREM', lease_set_key, key)
241
+ end
242
+ else
243
+ -- Lease key doesn't exist, remove from set
244
+ redis.call('SREM', lease_set_key, key)
245
+ end
246
+ end
247
+
248
+ -- Check if we have capacity
249
+ local available = capacity - current_usage
250
+ if available >= permit_count then
251
+ -- Set lease with TTL (value is just the permit count)
252
+ redis.call('SETEX', lease_key, expiration_seconds, permit_count)
253
+ -- Add lease key to the set
254
+ redis.call('SADD', lease_set_key, lease_key)
255
+ -- Set TTL on the set (4x the lease TTL to ensure cleanup)
256
+ redis.call('EXPIRE', lease_set_key, expiration_seconds * 4)
257
+
258
+ return {1, lease_key, current_usage + permit_count}
259
+ else
260
+ return {0, '', current_usage}
261
+ end
262
+ LUA
263
+ GET_USAGE_SCRIPT = T.let(<<~LUA, T.untyped)
264
+ local lease_set_key = KEYS[1]
265
+ local expiration_seconds = tonumber(ARGV[1])
266
+
267
+ -- Get all active leases from the set and calculate current usage
268
+ local lease_keys = redis.call('SMEMBERS', lease_set_key)
269
+ local current_usage = 0
270
+ local has_valid_leases = false
271
+
272
+ for i, lease_key in ipairs(lease_keys) do
273
+ local permits = redis.call('GET', lease_key)
274
+ if permits then
275
+ local permits_from_lease = tonumber(permits)
276
+ if permits_from_lease then
277
+ current_usage = current_usage + permits_from_lease
278
+ has_valid_leases = true
279
+ else
280
+ -- Remove lease with invalid permit count
281
+ redis.call('DEL', lease_key)
282
+ redis.call('SREM', lease_set_key, lease_key)
283
+ end
284
+ else
285
+ -- Lease key doesn't exist, remove from set
286
+ redis.call('SREM', lease_set_key, lease_key)
287
+ end
288
+ end
289
+
290
+ -- Refresh TTL on the set if there are valid leases (4x the lease TTL)
291
+ if has_valid_leases then
292
+ redis.call('EXPIRE', lease_set_key, expiration_seconds * 4)
293
+ end
294
+
295
+ return current_usage
296
+ LUA
297
+ RELEASE_LEASE_SCRIPT = T.let(<<~LUA, T.untyped)
298
+ local lease_key = KEYS[1]
299
+ local queue_key = KEYS[2]
300
+ local lease_set_key = KEYS[3]
301
+ local permit_count = tonumber(ARGV[1])
302
+ local max_signals = tonumber(ARGV[2])
303
+
304
+ -- Remove the lease
305
+ redis.call('DEL', lease_key)
306
+ -- Remove from the lease set
307
+ redis.call('SREM', lease_set_key, lease_key)
308
+
309
+ -- Signal waiting clients about the released permits
310
+ redis.call('LPUSH', queue_key, 'permits:' .. permit_count)
311
+
312
+ -- Trim queue to prevent indefinite growth (atomic)
313
+ redis.call('LTRIM', queue_key, 0, max_signals - 1)
314
+
315
+ return 1
316
+ LUA
317
+ GET_LEASE_SCRIPT_SHA = T.let(Digest::SHA1.hexdigest(GET_LEASE_SCRIPT), T.untyped)
318
+ GET_USAGE_SCRIPT_SHA = T.let(Digest::SHA1.hexdigest(GET_USAGE_SCRIPT), T.untyped)
319
+ RELEASE_LEASE_SCRIPT_SHA = T.let(Digest::SHA1.hexdigest(RELEASE_LEASE_SCRIPT), T.untyped)
320
+
321
+ # sord warn - Redis wasn't able to be resolved to a constant in this project
322
+ # sord warn - ConnectionPool wasn't able to be resolved to a constant in this project
323
+ # sord omit - no YARD type given for "lease_expiration_seconds:", using untyped
324
+ # Initialize the semaphore with a maximum capacity and required namespace.
325
+ #
326
+ # _@param_ `capacity` — Maximum number of concurrent operations allowed (also called permits)
327
+ #
328
+ # _@param_ `namespace` — Required namespace for Redis keys
329
+ #
330
+ # _@param_ `redis` — Optional Redis client or connection pool (defaults to new Redis instance)
331
+ #
332
+ # _@param_ `logger` — the logger
333
+ sig do
334
+ params(
335
+ capacity: Integer,
336
+ namespace: String,
337
+ redis: T.nilable(T.any(Redis, ConnectionPool)),
338
+ logger: Logger,
339
+ lease_expiration_seconds: T.untyped
340
+ ).void
341
+ end
342
+ def initialize(capacity, namespace, redis: nil, logger: CountingSemaphore::NullLogger, lease_expiration_seconds: LEASE_EXPIRATION_SECONDS); end
343
+
344
+ # Acquires the given number of permits from this semaphore, blocking until all are available.
345
+ #
346
+ # _@param_ `permits` — Number of permits to acquire (default: 1)
347
+ #
348
+ # _@return_ — A lease object that must be passed to release()
349
+ sig { params(permits: Integer).returns(CountingSemaphore::Lease) }
350
+ def acquire(permits = 1); end
351
+
352
+ # Releases a previously acquired lease, returning the permits to the semaphore.
353
+ #
354
+ # _@param_ `lease` — The lease object returned by acquire() or try_acquire()
355
+ sig { params(lease: CountingSemaphore::Lease).void }
356
+ def release(lease); end
357
+
358
+ # Acquires the given number of permits from this semaphore, only if all are available
359
+ # at the time of invocation or within the timeout interval.
360
+ #
361
+ # _@param_ `permits` — Number of permits to acquire (default: 1)
362
+ #
363
+ # _@param_ `timeout` — Number of seconds to wait, or nil to return immediately (default: nil). The timeout value will be rounded up to the nearest whole second due to Redis BLPOP limitations.
364
+ #
365
+ # _@return_ — A lease object if successful, nil otherwise
366
+ sig { params(permits: Integer, timeout: T.nilable(Numeric)).returns(T.nilable(CountingSemaphore::Lease)) }
367
+ def try_acquire(permits = 1, timeout: nil); end
368
+
369
+ # Returns the current number of permits available in this semaphore.
370
+ #
371
+ # _@return_ — Number of available permits
372
+ sig { returns(Integer) }
373
+ def available_permits; end
374
+
375
+ # Acquires and returns all permits that are immediately available.
376
+ # Note: For distributed semaphores, this may not be perfectly accurate due to race conditions.
377
+ #
378
+ # _@return_ — A lease for all available permits, or nil if none available
379
+ sig { returns(T.nilable(CountingSemaphore::Lease)) }
380
+ def drain_permits; end
381
+
382
+ # Returns debugging information about the current state of the semaphore.
383
+ # Includes current usage, capacity, available permits, and details about active leases.
384
+ #
385
+ # _@return_ — A hash containing :usage, :capacity, :available, and :active_leases
386
+ #
387
+ # ```ruby
388
+ # info = semaphore.debug_info
389
+ # puts "Usage: #{info[:usage]}/#{info[:capacity]}"
390
+ # info[:active_leases].each { |lease| puts "Lease: #{lease[:key]} - #{lease[:permits]} permits" }
391
+ # ```
392
+ sig { returns(T::Hash[T.untyped, T.untyped]) }
393
+ def debug_info; end
394
+
395
+ # sord warn - Redis wasn't able to be resolved to a constant in this project
396
+ # sord warn - ConnectionPool wasn't able to be resolved to a constant in this project
397
+ # Wraps a Redis client to support both ConnectionPool and bare Redis connections
398
+ #
399
+ # _@param_ `redis` — The Redis client or connection pool
400
+ #
401
+ # _@return_ — A wrapper that supports the `with` method
402
+ sig { params(redis: T.any(Redis, ConnectionPool)).returns(Object) }
403
+ def wrap_redis_client_with_pool(redis); end
404
+
405
+ # Executes a block with a Redis connection from the pool
406
+ #
407
+ # _@return_ — The result of the block
408
+ sig { params(block: T.untyped).returns(T.untyped) }
409
+ def with_redis(&block); end
410
+
411
+ # Executes a Redis script with automatic fallback to EVAL on NOSCRIPT error
412
+ #
413
+ # _@param_ `script_type` — The type of script (:get_lease, :release_lease, :get_usage)
414
+ #
415
+ # _@param_ `keys` — Keys for the script
416
+ #
417
+ # _@param_ `argv` — Arguments for the script
418
+ #
419
+ # _@return_ — The result of the script execution
420
+ sig { params(script_type: Symbol, keys: T::Array[T.untyped], argv: T::Array[T.untyped]).returns(T.untyped) }
421
+ def execute_script(script_type, keys: [], argv: []); end
422
+
423
+ # sord omit - no YARD type given for "permit_count", using untyped
424
+ # sord omit - no YARD type given for "timeout_seconds:", using untyped
425
+ # sord omit - no YARD return type given, using untyped
426
+ sig { params(permit_count: T.untyped, timeout_seconds: T.untyped).returns(T.untyped) }
427
+ def acquire_lease_internal(permit_count, timeout_seconds:); end
428
+
429
+ # sord omit - no YARD type given for "permit_count", using untyped
430
+ # sord omit - no YARD type given for "remaining_timeout", using untyped
431
+ # sord omit - no YARD return type given, using untyped
432
+ sig { params(permit_count: T.untyped, remaining_timeout: T.untyped).returns(T.untyped) }
433
+ def wait_for_permits(permit_count, remaining_timeout); end
434
+
435
+ # sord omit - no YARD type given for "permit_count", using untyped
436
+ # sord omit - no YARD return type given, using untyped
437
+ sig { params(permit_count: T.untyped).returns(T.untyped) }
438
+ def attempt_lease_acquisition(permit_count); end
439
+
440
+ # sord omit - no YARD type given for "lease_key", using untyped
441
+ # sord omit - no YARD type given for "permit_count", using untyped
442
+ # sord omit - no YARD return type given, using untyped
443
+ sig { params(lease_key: T.untyped, permit_count: T.untyped).returns(T.untyped) }
444
+ def release_lease(lease_key, permit_count); end
445
+
446
+ # sord omit - no YARD return type given, using untyped
447
+ sig { returns(T.untyped) }
448
+ def get_current_usage; end
449
+
450
+ # sord omit - no YARD return type given, using untyped
451
+ sig { returns(T.untyped) }
452
+ def generate_lease_id; end
453
+
454
+ # Acquire a lease for the specified number of permits and execute the block.
455
+ # Blocks until sufficient resources are available.
456
+ # Kept for backwards compatibility - wraps acquire/release.
457
+ #
458
+ # _@param_ `permit_count` — Number of permits to acquire (default: 1)
459
+ #
460
+ # _@param_ `timeout` — Maximum time in seconds to wait for lease acquisition (default: 30). For Redis-backed semaphores, the timeout value will be rounded up to the nearest whole second due to Redis BLPOP limitations.
461
+ #
462
+ # _@return_ — The result of the block
463
+ sig { params(permit_count: Integer, timeout: Numeric, blk: T.proc.params(lease: T.nilable(CountingSemaphore::Lease)).void).returns(T.untyped) }
464
+ def with_lease(permit_count = 1, timeout: 30, &blk); end
465
+
466
+ # Get the current number of permits currently acquired.
467
+ # Kept for backwards compatibility.
468
+ #
469
+ # _@return_ — Number of permits currently in use
470
+ sig { returns(Integer) }
471
+ def currently_leased; end
472
+
473
+ sig { returns(Integer) }
474
+ attr_reader :capacity
475
+
476
+ # Null pool for bare Redis connections that don't need connection pooling.
477
+ # Provides a compatible interface with ConnectionPool for bare Redis instances.
478
+ class NullPool
479
+ # sord warn - Redis wasn't able to be resolved to a constant in this project
480
+ # Creates a new NullPool wrapper around a Redis connection.
481
+ #
482
+ # _@param_ `redis_connection` — The Redis connection to wrap
483
+ sig { params(redis_connection: Redis).void }
484
+ def initialize(redis_connection); end
485
+
486
+ # Yields the wrapped Redis connection to the block.
487
+ # Provides ConnectionPool-compatible interface.
488
+ #
489
+ # _@return_ — The result of the block
490
+ sig { params(block: T.untyped).returns(T.untyped) }
491
+ def with(&block); end
492
+ end
493
+ end
494
+
495
+ # Module providing backwards-compatible with_lease method
496
+ # Requires the including class to implement: acquire, release, capacity
497
+ module WithLeaseSupport
498
+ # Acquire a lease for the specified number of permits and execute the block.
499
+ # Blocks until sufficient resources are available.
500
+ # Kept for backwards compatibility - wraps acquire/release.
501
+ #
502
+ # _@param_ `permit_count` — Number of permits to acquire (default: 1)
503
+ #
504
+ # _@param_ `timeout` — Maximum time in seconds to wait for lease acquisition (default: 30). For Redis-backed semaphores, the timeout value will be rounded up to the nearest whole second due to Redis BLPOP limitations.
505
+ #
506
+ # _@return_ — The result of the block
507
+ sig { params(permit_count: Integer, timeout: Numeric, blk: T.proc.params(lease: T.nilable(CountingSemaphore::Lease)).void).returns(T.untyped) }
508
+ def with_lease(permit_count = 1, timeout: 30, &blk); end
509
+
510
+ # Get the current number of permits currently acquired.
511
+ # Kept for backwards compatibility.
512
+ #
513
+ # _@return_ — Number of permits currently in use
514
+ sig { returns(Integer) }
515
+ def currently_leased; end
516
+ end
517
+ end