1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
48
49
50
51
52
53
54
55
56
57 @Aspect
58 @SuppressWarnings("PMD.DoNotUseThreads")
59 public final class MethodInterrupter {
60
61
62
63
64 private final transient Set<MethodInterrupter.Call> calls;
65
66
67
68
69 private final transient ScheduledExecutorService interrupter;
70
71
72
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
93
94
95
96
97
98
99
100
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
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
129
130
131
132 private static final class Call implements
133 Comparable<MethodInterrupter.Call> {
134
135
136
137 private final transient Thread thread;
138
139
140
141
142 private final transient long start;
143
144
145
146
147 private final transient long deadline;
148
149
150
151
152 private final transient ProceedingJoinPoint point;
153
154
155
156
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
186
187
188 public boolean expired() {
189 return this.deadline < System.currentTimeMillis();
190 }
191
192
193
194
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 }