View Javadoc
1   /*
2    * Copyright (c) 2012-2024, jcabi.com
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the jcabi.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package com.jcabi.aspects.aj;
31  
32  import com.jcabi.aspects.Timeable;
33  import com.jcabi.log.Logger;
34  import com.jcabi.log.VerboseRunnable;
35  import java.lang.reflect.Method;
36  import java.util.Set;
37  import java.util.concurrent.ConcurrentSkipListSet;
38  import java.util.concurrent.Executors;
39  import java.util.concurrent.ScheduledExecutorService;
40  import java.util.concurrent.TimeUnit;
41  import org.aspectj.lang.ProceedingJoinPoint;
42  import org.aspectj.lang.annotation.Around;
43  import org.aspectj.lang.annotation.Aspect;
44  import org.aspectj.lang.reflect.MethodSignature;
45  
46  /**
47   * Interrupts long-running methods.
48   *
49   * <p>It is an AspectJ aspect and you are not supposed to use it directly. It
50   * is instantiated by AspectJ runtime framework when your code is annotated
51   * with {@link Timeable} annotation.
52   *
53   * <p>The class is thread-safe.
54   *
55   * @since 0.7.16
56   */
57  @Aspect
58  @SuppressWarnings("PMD.DoNotUseThreads")
59  public final class MethodInterrupter {
60  
61      /**
62       * Calls being watched.
63       */
64      private final transient Set<MethodInterrupter.Call> calls;
65  
66      /**
67       * Service that interrupts threads.
68       */
69      private final transient ScheduledExecutorService interrupter;
70  
71      /**
72       * Public ctor.
73       */
74      @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors")
75      public MethodInterrupter() {
76          this.calls = new ConcurrentSkipListSet<>();
77          this.interrupter = Executors.newSingleThreadScheduledExecutor(
78              new NamedThreads(
79                  "timeable",
80                  "interrupting of @Timeable annotated methods"
81              )
82          );
83          this.interrupter.scheduleWithFixedDelay(
84              new VerboseRunnable(
85                  this::interrupt
86              ),
87              1L, 1L, TimeUnit.SECONDS
88          );
89      }
90  
91      /**
92       * Run and interrupt a method, if stuck.
93       *
94       * <p>Try NOT to change the signature of this method, in order to keep
95       * it backward compatible.
96       *
97       * @param point Joint point
98       * @return The result of call
99       * @throws Throwable If something goes wrong inside
100      * @checkstyle IllegalThrows (5 lines)
101      */
102     @Around("execution(* * (..)) && @annotation(com.jcabi.aspects.Timeable)")
103     @SuppressWarnings("PMD.AvoidCatchingThrowable")
104     public Object wrap(final ProceedingJoinPoint point) throws Throwable {
105         final MethodInterrupter.Call call = new MethodInterrupter.Call(point);
106         this.calls.add(call);
107         final Object output;
108         try {
109             output = point.proceed();
110         } finally {
111             this.calls.remove(call);
112         }
113         return output;
114     }
115 
116     /**
117      * Interrupt threads when needed.
118      */
119     private void interrupt() {
120         synchronized (this.interrupter) {
121             this.calls.removeIf(
122                 call -> call.expired() && call.interrupted()
123             );
124         }
125     }
126 
127     /**
128      * A call being watched.
129      *
130      * @since 0.7.16
131      */
132     private static final class Call implements
133         Comparable<MethodInterrupter.Call> {
134         /**
135          * The thread called.
136          */
137         private final transient Thread thread;
138 
139         /**
140          * When started.
141          */
142         private final transient long start;
143 
144         /**
145          * When will expire.
146          */
147         private final transient long deadline;
148 
149         /**
150          * Join point.
151          */
152         private final transient ProceedingJoinPoint point;
153 
154         /**
155          * Public ctor.
156          * @param pnt Joint point
157          */
158         @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors")
159         Call(final ProceedingJoinPoint pnt) {
160             this.thread = Thread.currentThread();
161             this.start = System.currentTimeMillis();
162             this.point = pnt;
163             final Method method = ((MethodSignature) pnt.getSignature())
164                 .getMethod();
165             final Timeable annt = method.getAnnotation(Timeable.class);
166             this.deadline = this.start + annt.unit().toMillis(
167                 (long) annt.limit()
168             );
169         }
170 
171         @Override
172         public int compareTo(final MethodInterrupter.Call obj) {
173             final int compare;
174             if (this.deadline > obj.deadline) {
175                 compare = 1;
176             } else if (this.deadline < obj.deadline) {
177                 compare = -1;
178             } else {
179                 compare = 0;
180             }
181             return compare;
182         }
183 
184         /**
185          * Is it expired already?
186          * @return TRUE if expired
187          */
188         public boolean expired() {
189             return this.deadline < System.currentTimeMillis();
190         }
191 
192         /**
193          * This thread is stopped already (interrupt if not)?
194          * @return TRUE if it's already dead
195          */
196         public boolean interrupted() {
197             final boolean dead;
198             if (this.thread.isAlive()) {
199                 this.thread.interrupt();
200                 final Method method = ((MethodSignature) this.point.getSignature())
201                     .getMethod();
202                 if (Logger.isWarnEnabled(method.getDeclaringClass())) {
203                     Logger.warn(
204                         method.getDeclaringClass(),
205                         "%s: interrupted on %[ms]s timeout (over %[ms]s)",
206                         Mnemos.toText(this.point, true, false),
207                         System.currentTimeMillis() - this.start,
208                         this.deadline - this.start
209                     );
210                 }
211                 dead = false;
212             } else {
213                 dead = true;
214             }
215             return dead;
216         }
217     }
218 
219 }