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 */