001/* 002 * ============================================================================ 003 * Copyright © 2002-2026 by Thomas Thrien. 004 * All Rights Reserved. 005 * ============================================================================ 006 * Licensed to the public under the agreements of the GNU Lesser General Public 007 * License, version 3.0 (the "License"). You may obtain a copy of the License at 008 * 009 * http://www.gnu.org/licenses/lgpl.html 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 014 * License for the specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package org.tquadrat.foundation.lang.internal; 019 020import static java.util.UUID.randomUUID; 021import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; 022import static java.util.concurrent.TimeUnit.MILLISECONDS; 023import static org.apiguardian.api.API.Status.INTERNAL; 024import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 025 026import java.io.Serial; 027import java.lang.ref.Cleaner; 028import java.lang.ref.Cleaner.Cleanable; 029import java.time.Duration; 030import java.time.Instant; 031import java.util.LinkedHashMap; 032import java.util.List; 033import java.util.Map; 034import java.util.TimerTask; 035import java.util.UUID; 036import java.util.concurrent.ScheduledExecutorService; 037import java.util.concurrent.Semaphore; 038 039import org.apiguardian.api.API; 040import org.tquadrat.foundation.annotation.ClassVersion; 041import org.tquadrat.foundation.lang.AutoLock; 042import org.tquadrat.foundation.lang.AutoSemaphore; 043 044/** 045 * An implementation for 046 * {@link org.tquadrat.foundation.lang.AutoSemaphore} 047 * that allows a timeout for the permits. 048 * 049 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 050 * @version $Id: TimeoutSemaphoreImpl.java 1258 2026-06-04 18:33:06Z tquadrat $ 051 * @since 0.25.2 052 * 053 * @UMLGraph.link 054 */ 055@SuppressWarnings( "SerializableDeserializableClassInSecureContext" ) 056@ClassVersion( sourceVersion = "$Id: TimeoutSemaphoreImpl.java 1258 2026-06-04 18:33:06Z tquadrat $" ) 057@API( status = INTERNAL, since = "0.25.2" ) 058public final class TimeoutSemaphoreImpl extends Semaphore implements AutoSemaphore 059{ 060 /*---------------*\ 061 ====** Inner Classes **==================================================== 062 \*---------------*/ 063 /** 064 * <p>{@summary The {@code Janitor} for the owning 065 * {@link TimeoutSemaphoreImpl} 066 * instance.}</p> 067 * 068 * @param reaperExecutor The reference for the 069 * {@link TimeoutSemaphoreImpl#m_ReaperExecutor}. 070 * 071 * @version $Id: TimeoutSemaphoreImpl.java 1258 2026-06-04 18:33:06Z tquadrat $ 072 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 073 * @UMLGraph.link 074 * @since 0.25.2 075 */ 076 @SuppressWarnings( "NewClassNamingConvention" ) 077 @ClassVersion( sourceVersion = "$Id: TimeoutSemaphoreImpl.java 1258 2026-06-04 18:33:06Z tquadrat $" ) 078 @API( status = INTERNAL, since = "0.25.2" ) 079 private record Janitor( ScheduledExecutorService reaperExecutor ) implements Runnable 080 { 081 /*---------*\ 082 ====** Methods **====================================================== 083 \*---------*/ 084 /** 085 * Performs the housekeeping. 086 */ 087 @Override 088 public final void run() 089 { 090 //---* Kill the executor *----------------------------------------- 091 reaperExecutor.close(); 092 } // run() 093 } 094 // class Janitor 095 096 /** 097 * <p>{@summary The reaper thread.}</p> 098 * 099 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 100 * @version $Id: TimeoutSemaphoreImpl.java 1258 2026-06-04 18:33:06Z tquadrat $ 101 * @since 0.25.2 102 * 103 * @UMLGraph.link 104 */ 105 @SuppressWarnings( {"resource", "NewClassNamingConvention"} ) 106 @ClassVersion( sourceVersion = "$Id: TimeoutSemaphoreImpl.java 1258 2026-06-04 18:33:06Z tquadrat $" ) 107 @API( status = INTERNAL, since = "0.25.2" ) 108 private final class Reaper extends TimerTask 109 { 110 /*--------------*\ 111 ====** Constructors **================================================= 112 \*--------------*/ 113 /** 114 * Creates a new instance of {@code Reaper}. 115 */ 116 public Reaper() { /* Just exists */ } 117 118 /*---------*\ 119 ====** Methods **====================================================== 120 \*---------*/ 121 /** 122 * {@inheritDoc} 123 */ 124 @Override 125 public final void run() 126 { 127 final var now = Instant.now(); 128 try( final var _ = m_Lock.lock() ) 129 { 130 final var tokens = List.copyOf( m_Registry.values() ); 131 for( final var token : tokens ) 132 { 133 if( token.getEndOfLife().isBefore( now ) ) 134 { 135 m_Registry.remove( token.getId() ); 136 } 137 } 138 } 139 } // run() 140 } 141 // class Reaper 142 143 /** 144 * <p>{@summary The token that holds the permits to be released when a 145 * {@code try-with-resources} block is left or the timeout has 146 * expired.}</p> 147 * 148 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 149 * @version $Id: TimeoutSemaphoreImpl.java 1258 2026-06-04 18:33:06Z tquadrat $ 150 * @since 0.25.2 151 * 152 * @UMLGraph.link 153 */ 154 @ClassVersion( sourceVersion = "$Id: TimeoutSemaphoreImpl.java 1258 2026-06-04 18:33:06Z tquadrat $" ) 155 @API( status = INTERNAL, since = "0.25.2" ) 156 public final class TokenImpl implements AutoSemaphore.Token 157 { 158 /*------------*\ 159 ====** Attributes **=================================================== 160 \*------------*/ 161 /** 162 * The end-of-life for this permit. 163 */ 164 private final Instant m_EndOfLife; 165 166 /** 167 * The id for this permit. 168 */ 169 @SuppressWarnings( "FieldNamingConvention" ) 170 private final UUID m_Id; 171 172 /** 173 * The number of permits to release on close. 174 */ 175 private final int m_Permits; 176 177 /*--------------*\ 178 ====** Constructors **================================================= 179 \*--------------*/ 180 /** 181 * Creates a new instance of {@code TokenImpl}. 182 * 183 * @param permits The number of the acquired permits. 184 * @param endOfLife The end-of-life for this permit. 185 */ 186 public TokenImpl( final int permits, final Instant endOfLife ) 187 { 188 m_Permits = permits; 189 m_EndOfLife = requireNonNullArgument( endOfLife, "endOfLife" ); 190 m_Id = randomUUID(); 191 } // TokenImpl() 192 193 /*---------*\ 194 ====** Methods **====================================================== 195 \*---------*/ 196 /** 197 * {@inheritDoc} 198 */ 199 @Override 200 public final void close() 201 { 202 TimeoutSemaphoreImpl.this.release( m_Id ); 203 } // close() 204 205 /** 206 * Returns the time for the end-of-life. 207 * 208 * @return The end-of-life instant. 209 */ 210 public final Instant getEndOfLife() { return m_EndOfLife; } 211 212 /** 213 * Returns the id of the token. 214 * 215 * @return The id. 216 */ 217 public final UUID getId() { return m_Id; } 218 219 /** 220 * Returns the number of permits for this token. 221 * 222 * @return The number of permits. 223 */ 224 public final int getPermits() { return m_Permits; } 225 226 /** 227 * {@inheritDoc} 228 */ 229 @Override 230 public final Semaphore getSemaphore() { return TimeoutSemaphoreImpl.this; } 231 } 232 // class TokenImpl 233 234 /*-----------*\ 235 ====** Constants **======================================================== 236 \*-----------*/ 237 /** 238 * The cycle time for the reaper thread in milliseconds: {@value} ms. 239 */ 240 public static final long REAPER_CYCLE = 1000L; 241 242 /*------------*\ 243 ====** Attributes **======================================================= 244 \*------------*/ 245 /** 246 * <p>{@summary The 247 * {@link Cleanable} 248 * for this instance.} As instances of this class does not support 249 * {@code close()} or something similar, keeping the reference to the 250 * {@code Cleanable} is considered obsolete. But we keep it anyway, in 251 * case this changes.</p> 252 */ 253 @SuppressWarnings( {"unused", "FieldCanBeLocal"} ) 254 private final transient Cleanable m_Cleanable; 255 256 /** 257 * The janitor for instances of this class. 258 */ 259 @SuppressWarnings( {"UseOfConcreteClass", "FieldCanBeLocal"} ) 260 private final transient Janitor m_Janitor; 261 262 /** 263 * The lock that guards the access to 264 * {@link #m_Registry}. 265 */ 266 private final transient AutoLock m_Lock; 267 268 /** 269 * The timeout duration. 270 * 271 * @serial 272 */ 273 private final Duration m_Timeout; 274 275 /** 276 * The executor for the reaper. 277 */ 278 private final transient ScheduledExecutorService m_ReaperExecutor; 279 280 /** 281 * The permit registry. 282 */ 283 private final transient Map<UUID,TokenImpl> m_Registry = new LinkedHashMap<>(); 284 285 /*------------------------*\ 286 ====** Static Initialisations **=========================================== 287 \*------------------------*/ 288 /** 289 * The serial version UID for objects of this class: {@value}. 290 * 291 * @hidden 292 */ 293 @Serial 294 private static final long serialVersionUID = 539879857L; 295 296 /** 297 * The cleaner that is used for the instances of this class. 298 */ 299 private static final Cleaner m_Cleaner; 300 301 static 302 { 303 m_Cleaner = Cleaner.create(); 304 } 305 306 /*--------------*\ 307 ====** Constructors **===================================================== 308 \*--------------*/ 309 /** 310 * Creates an {@code TimeoutSemaphoreImpl} instance with the given number 311 * of permits, the given timeout duration and non-fair fairness setting. 312 * 313 * @param permits The initial number of permits available. This value may 314 * be negative, in which case releases must occur before any acquires 315 * will be granted. 316 * @param timeout The timeout. 317 */ 318 public TimeoutSemaphoreImpl( final int permits, final Duration timeout ) 319 { 320 this( permits, false, timeout ); 321 } // TimeoutSemaphoreImpl() 322 323 /** 324 * Creates an {@code TimeoutSemaphoreImpl} instance with the given number 325 * of permits, the given timeout duration and the given fairness setting. 326 * 327 * @param permits The initial number of permits available. This value may 328 * be negative, in which case releases must occur before any acquires 329 * will be granted. 330 * @param fair {@true} if this semaphore will guarantee first-in 331 * first-out granting of permits under contention, else {@false}. 332 * @param timeout The timeout. 333 */ 334 public TimeoutSemaphoreImpl( final int permits, final boolean fair, final Duration timeout ) 335 { 336 super( permits, fair ); 337 m_Timeout = requireNonNullArgument( timeout, "timeout" ); 338 m_Lock = AutoLock.of(); 339 340 m_ReaperExecutor = newSingleThreadScheduledExecutor(); 341 m_ReaperExecutor.scheduleWithFixedDelay( new Reaper(), REAPER_CYCLE, REAPER_CYCLE, MILLISECONDS ); 342 343 m_Janitor = new Janitor( m_ReaperExecutor ); 344 m_Cleanable = registerJanitor( m_Janitor ); 345 } // TimeoutSemaphoreImpl() 346 347 /*---------*\ 348 ====** Methods **========================================================== 349 \*---------*/ 350 /** 351 * {@inheritDoc} 352 */ 353 @SuppressWarnings( "ReturnOfInnerClass" ) 354 @Override 355 public final Token acquireToken( final int permits ) throws InterruptedException, IllegalArgumentException 356 { 357 acquire( permits ); 358 final var retValue = createToken( permits ); 359 360 //---* Done *---------------------------------------------------------- 361 return retValue; 362 } // acquireToken() 363 364 /** 365 * {@inheritDoc} 366 */ 367 @SuppressWarnings( "ReturnOfInnerClass" ) 368 @Override 369 public final Token acquireTokenUninterruptibly( final int permits ) throws IllegalArgumentException 370 { 371 acquireUninterruptibly( permits ); 372 final var retValue = createToken( permits ); 373 374 //---* Done *---------------------------------------------------------- 375 return retValue; 376 } // acquireTokenUninterruptibly() 377 378 /** 379 * Creates a token and adds to the registry. 380 * 381 * @param permits The number of permits to acquire. 382 * @return The new token. 383 */ 384 private final Token createToken( final int permits ) 385 { 386 final var endOfLife = Instant.now().plus( m_Timeout ); 387 final var retValue = new TokenImpl( permits, endOfLife ); 388 m_Lock.execute( () -> m_Registry.put( retValue.getId(), retValue ) ); 389 390 //---* Done *---------------------------------------------------------- 391 return retValue; 392 } // createToken() 393 394 /** 395 * {@inheritDoc} 396 */ 397 @Override 398 public final Semaphore getSemaphore() { return this; } 399 400 /** 401 * Registers the janitor that is doing the housekeeping on garbage 402 * collection. 403 * 404 * @param janitor The janitor. 405 * @return The 406 * {@link Cleanable} 407 * for this instance. 408 */ 409 private final Cleanable registerJanitor( final Runnable janitor ) 410 { 411 final var retValue = m_Cleaner.register( this, requireNonNullArgument( janitor, "janitor" ) ); 412 413 //---* Done *---------------------------------------------------------- 414 return retValue; 415 } // registerJanitor() 416 417 /** 418 * <p>{@summary Releases the number of permits associated with the token 419 * with the given id, returning them to the semaphore.}</p> 420 * <p>Releases that number of permits, increasing the number of available 421 * permits by that amount.</p> 422 * <p>Nothing happens if the token with given id had died already.</p> 423 * 424 * @param id The id of the token. 425 */ 426 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 427 public final void release( final UUID id ) 428 { 429 m_Lock.execute( () -> m_Registry.remove( id ) ) 430 .ifPresent( token -> release( token.getPermits() ) ); 431 } // release() 432} 433// class TimeoutSemaphoreImpl 434 435/* 436 * End of File 437 */