• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.util.concurrent;
18 
19 import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION;
20 import static com.google.common.base.StandardSystemProperty.OS_NAME;
21 import static com.google.common.truth.Truth.assertThat;
22 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
23 import static java.util.Arrays.asList;
24 import static java.util.concurrent.TimeUnit.SECONDS;
25 import static org.junit.Assert.assertThrows;
26 
27 import com.google.common.collect.ImmutableMap;
28 import com.google.common.collect.ImmutableSet;
29 import com.google.common.collect.Lists;
30 import com.google.common.collect.Sets;
31 import com.google.common.testing.NullPointerTester;
32 import com.google.common.testing.TestLogHandler;
33 import com.google.common.util.concurrent.Service.State;
34 import com.google.common.util.concurrent.ServiceManager.Listener;
35 import java.time.Duration;
36 import java.util.Arrays;
37 import java.util.Collection;
38 import java.util.List;
39 import java.util.Set;
40 import java.util.concurrent.CountDownLatch;
41 import java.util.concurrent.Executor;
42 import java.util.concurrent.TimeUnit;
43 import java.util.concurrent.TimeoutException;
44 import java.util.logging.Formatter;
45 import java.util.logging.Level;
46 import java.util.logging.LogRecord;
47 import java.util.logging.Logger;
48 import junit.framework.TestCase;
49 
50 /**
51  * Tests for {@link ServiceManager}.
52  *
53  * @author Luke Sandberg
54  * @author Chris Nokleberg
55  */
56 public class ServiceManagerTest extends TestCase {
57 
58   private static class NoOpService extends AbstractService {
59     @Override
doStart()60     protected void doStart() {
61       notifyStarted();
62     }
63 
64     @Override
doStop()65     protected void doStop() {
66       notifyStopped();
67     }
68   }
69 
70   /*
71    * A NoOp service that will delay the startup and shutdown notification for a configurable amount
72    * of time.
73    */
74   private static class NoOpDelayedService extends NoOpService {
75     private long delay;
76 
NoOpDelayedService(long delay)77     public NoOpDelayedService(long delay) {
78       this.delay = delay;
79     }
80 
81     @Override
doStart()82     protected void doStart() {
83       new Thread() {
84         @Override
85         public void run() {
86           Uninterruptibles.sleepUninterruptibly(delay, TimeUnit.MILLISECONDS);
87           notifyStarted();
88         }
89       }.start();
90     }
91 
92     @Override
doStop()93     protected void doStop() {
94       new Thread() {
95         @Override
96         public void run() {
97           Uninterruptibles.sleepUninterruptibly(delay, TimeUnit.MILLISECONDS);
98           notifyStopped();
99         }
100       }.start();
101     }
102   }
103 
104   private static class FailStartService extends NoOpService {
105     @Override
doStart()106     protected void doStart() {
107       notifyFailed(new IllegalStateException("start failure"));
108     }
109   }
110 
111   private static class FailRunService extends NoOpService {
112     @Override
doStart()113     protected void doStart() {
114       super.doStart();
115       notifyFailed(new IllegalStateException("run failure"));
116     }
117   }
118 
119   private static class FailStopService extends NoOpService {
120     @Override
doStop()121     protected void doStop() {
122       notifyFailed(new IllegalStateException("stop failure"));
123     }
124   }
125 
testServiceStartupTimes()126   public void testServiceStartupTimes() {
127     if (isWindows() && isJava8()) {
128       // Flaky there: https://github.com/google/guava/pull/6731#issuecomment-1736298607
129       return;
130     }
131     Service a = new NoOpDelayedService(150);
132     Service b = new NoOpDelayedService(353);
133     ServiceManager serviceManager = new ServiceManager(asList(a, b));
134     serviceManager.startAsync().awaitHealthy();
135     ImmutableMap<Service, Long> startupTimes = serviceManager.startupTimes();
136     assertThat(startupTimes).hasSize(2);
137     assertThat(startupTimes.get(a)).isAtLeast(150);
138     assertThat(startupTimes.get(b)).isAtLeast(353);
139   }
140 
testServiceStartupDurations()141   public void testServiceStartupDurations() {
142     if (isWindows() && isJava8()) {
143       // Flaky there: https://github.com/google/guava/pull/6731#issuecomment-1736298607
144       return;
145     }
146     Service a = new NoOpDelayedService(150);
147     Service b = new NoOpDelayedService(353);
148     ServiceManager serviceManager = new ServiceManager(asList(a, b));
149     serviceManager.startAsync().awaitHealthy();
150     ImmutableMap<Service, Duration> startupTimes = serviceManager.startupDurations();
151     assertThat(startupTimes).hasSize(2);
152     assertThat(startupTimes.get(a)).isAtLeast(Duration.ofMillis(150));
153     assertThat(startupTimes.get(b)).isAtLeast(Duration.ofMillis(353));
154   }
155 
testServiceStartupTimes_selfStartingServices()156   public void testServiceStartupTimes_selfStartingServices() {
157     // This tests to ensure that:
158     // 1. service times are accurate when the service is started by the manager
159     // 2. service times are recorded when the service is not started by the manager (but they may
160     // not be accurate).
161     final Service b =
162         new NoOpDelayedService(353) {
163           @Override
164           protected void doStart() {
165             super.doStart();
166             // This will delay service listener execution at least 150 milliseconds
167             Uninterruptibles.sleepUninterruptibly(150, TimeUnit.MILLISECONDS);
168           }
169         };
170     Service a =
171         new NoOpDelayedService(150) {
172           @Override
173           protected void doStart() {
174             b.startAsync();
175             super.doStart();
176           }
177         };
178     ServiceManager serviceManager = new ServiceManager(asList(a, b));
179     serviceManager.startAsync().awaitHealthy();
180     ImmutableMap<Service, Long> startupTimes = serviceManager.startupTimes();
181     assertThat(startupTimes).hasSize(2);
182     assertThat(startupTimes.get(a)).isAtLeast(150);
183     // Service b startup takes at least 353 millis, but starting the timer is delayed by at least
184     // 150 milliseconds. so in a perfect world the timing would be 353-150=203ms, but since either
185     // of our sleep calls can be arbitrarily delayed we should just assert that there is a time
186     // recorded.
187     assertThat(startupTimes.get(b)).isNotNull();
188   }
189 
testServiceStartStop()190   public void testServiceStartStop() {
191     Service a = new NoOpService();
192     Service b = new NoOpService();
193     ServiceManager manager = new ServiceManager(asList(a, b));
194     RecordingListener listener = new RecordingListener();
195     manager.addListener(listener, directExecutor());
196     assertState(manager, Service.State.NEW, a, b);
197     assertFalse(manager.isHealthy());
198     manager.startAsync().awaitHealthy();
199     assertState(manager, Service.State.RUNNING, a, b);
200     assertTrue(manager.isHealthy());
201     assertTrue(listener.healthyCalled);
202     assertFalse(listener.stoppedCalled);
203     assertTrue(listener.failedServices.isEmpty());
204     manager.stopAsync().awaitStopped();
205     assertState(manager, Service.State.TERMINATED, a, b);
206     assertFalse(manager.isHealthy());
207     assertTrue(listener.stoppedCalled);
208     assertTrue(listener.failedServices.isEmpty());
209   }
210 
testFailStart()211   public void testFailStart() throws Exception {
212     Service a = new NoOpService();
213     Service b = new FailStartService();
214     Service c = new NoOpService();
215     Service d = new FailStartService();
216     Service e = new NoOpService();
217     ServiceManager manager = new ServiceManager(asList(a, b, c, d, e));
218     RecordingListener listener = new RecordingListener();
219     manager.addListener(listener, directExecutor());
220     assertState(manager, Service.State.NEW, a, b, c, d, e);
221     assertThrows(IllegalStateException.class, () -> manager.startAsync().awaitHealthy());
222     assertFalse(listener.healthyCalled);
223     assertState(manager, Service.State.RUNNING, a, c, e);
224     assertEquals(ImmutableSet.of(b, d), listener.failedServices);
225     assertState(manager, Service.State.FAILED, b, d);
226     assertFalse(manager.isHealthy());
227 
228     manager.stopAsync().awaitStopped();
229     assertFalse(manager.isHealthy());
230     assertFalse(listener.healthyCalled);
231     assertTrue(listener.stoppedCalled);
232   }
233 
testFailRun()234   public void testFailRun() throws Exception {
235     Service a = new NoOpService();
236     Service b = new FailRunService();
237     ServiceManager manager = new ServiceManager(asList(a, b));
238     RecordingListener listener = new RecordingListener();
239     manager.addListener(listener, directExecutor());
240     assertState(manager, Service.State.NEW, a, b);
241     assertThrows(IllegalStateException.class, () -> manager.startAsync().awaitHealthy());
242     assertTrue(listener.healthyCalled);
243     assertEquals(ImmutableSet.of(b), listener.failedServices);
244 
245     manager.stopAsync().awaitStopped();
246     assertState(manager, Service.State.FAILED, b);
247     assertState(manager, Service.State.TERMINATED, a);
248 
249     assertTrue(listener.stoppedCalled);
250   }
251 
testFailStop()252   public void testFailStop() throws Exception {
253     Service a = new NoOpService();
254     Service b = new FailStopService();
255     Service c = new NoOpService();
256     ServiceManager manager = new ServiceManager(asList(a, b, c));
257     RecordingListener listener = new RecordingListener();
258     manager.addListener(listener, directExecutor());
259 
260     manager.startAsync().awaitHealthy();
261     assertTrue(listener.healthyCalled);
262     assertFalse(listener.stoppedCalled);
263     manager.stopAsync().awaitStopped();
264 
265     assertTrue(listener.stoppedCalled);
266     assertEquals(ImmutableSet.of(b), listener.failedServices);
267     assertState(manager, Service.State.FAILED, b);
268     assertState(manager, Service.State.TERMINATED, a, c);
269   }
270 
testToString()271   public void testToString() throws Exception {
272     Service a = new NoOpService();
273     Service b = new FailStartService();
274     ServiceManager manager = new ServiceManager(asList(a, b));
275     String toString = manager.toString();
276     assertThat(toString).contains("NoOpService");
277     assertThat(toString).contains("FailStartService");
278   }
279 
testTimeouts()280   public void testTimeouts() throws Exception {
281     Service a = new NoOpDelayedService(50);
282     ServiceManager manager = new ServiceManager(asList(a));
283     manager.startAsync();
284     assertThrows(TimeoutException.class, () -> manager.awaitHealthy(1, TimeUnit.MILLISECONDS));
285     manager.awaitHealthy(5, SECONDS); // no exception thrown
286 
287     manager.stopAsync();
288     assertThrows(TimeoutException.class, () -> manager.awaitStopped(1, TimeUnit.MILLISECONDS));
289     manager.awaitStopped(5, SECONDS); // no exception thrown
290   }
291 
292   /**
293    * This covers a case where if the last service to stop failed then the stopped callback would
294    * never be called.
295    */
testSingleFailedServiceCallsStopped()296   public void testSingleFailedServiceCallsStopped() {
297     Service a = new FailStartService();
298     ServiceManager manager = new ServiceManager(asList(a));
299     RecordingListener listener = new RecordingListener();
300     manager.addListener(listener, directExecutor());
301     assertThrows(IllegalStateException.class, () -> manager.startAsync().awaitHealthy());
302     assertTrue(listener.stoppedCalled);
303   }
304 
305   /**
306    * This covers a bug where listener.healthy would get called when a single service failed during
307    * startup (it occurred in more complicated cases also).
308    */
testFailStart_singleServiceCallsHealthy()309   public void testFailStart_singleServiceCallsHealthy() {
310     Service a = new FailStartService();
311     ServiceManager manager = new ServiceManager(asList(a));
312     RecordingListener listener = new RecordingListener();
313     manager.addListener(listener, directExecutor());
314     assertThrows(IllegalStateException.class, () -> manager.startAsync().awaitHealthy());
315     assertFalse(listener.healthyCalled);
316   }
317 
318   /**
319    * This covers a bug where if a listener was installed that would stop the manager if any service
320    * fails and something failed during startup before service.start was called on all the services,
321    * then awaitStopped would deadlock due to an IllegalStateException that was thrown when trying to
322    * stop the timer(!).
323    */
testFailStart_stopOthers()324   public void testFailStart_stopOthers() throws TimeoutException {
325     Service a = new FailStartService();
326     Service b = new NoOpService();
327     final ServiceManager manager = new ServiceManager(asList(a, b));
328     manager.addListener(
329         new Listener() {
330           @Override
331           public void failure(Service service) {
332             manager.stopAsync();
333           }
334         },
335         directExecutor());
336     manager.startAsync();
337     manager.awaitStopped(10, TimeUnit.MILLISECONDS);
338   }
339 
testDoCancelStart()340   public void testDoCancelStart() throws TimeoutException {
341     Service a =
342         new AbstractService() {
343           @Override
344           protected void doStart() {
345             // Never starts!
346           }
347 
348           @Override
349           protected void doCancelStart() {
350             assertThat(state()).isEqualTo(Service.State.STOPPING);
351             notifyStopped();
352           }
353 
354           @Override
355           protected void doStop() {
356             throw new AssertionError(); // Should not be called.
357           }
358         };
359 
360     final ServiceManager manager = new ServiceManager(asList(a));
361     manager.startAsync();
362     manager.stopAsync();
363     manager.awaitStopped(10, TimeUnit.MILLISECONDS);
364     assertThat(manager.servicesByState().keySet()).containsExactly(Service.State.TERMINATED);
365   }
366 
testNotifyStoppedAfterFailure()367   public void testNotifyStoppedAfterFailure() throws TimeoutException {
368     Service a =
369         new AbstractService() {
370           @Override
371           protected void doStart() {
372             notifyFailed(new IllegalStateException("start failure"));
373             notifyStopped(); // This will be a no-op.
374           }
375 
376           @Override
377           protected void doStop() {
378             notifyStopped();
379           }
380         };
381     final ServiceManager manager = new ServiceManager(asList(a));
382     manager.startAsync();
383     manager.awaitStopped(10, TimeUnit.MILLISECONDS);
384     assertThat(manager.servicesByState().keySet()).containsExactly(Service.State.FAILED);
385   }
386 
assertState( ServiceManager manager, Service.State state, Service... services)387   private static void assertState(
388       ServiceManager manager, Service.State state, Service... services) {
389     Collection<Service> managerServices = manager.servicesByState().get(state);
390     for (Service service : services) {
391       assertEquals(service.toString(), state, service.state());
392       assertEquals(service.toString(), service.isRunning(), state == Service.State.RUNNING);
393       assertTrue(managerServices + " should contain " + service, managerServices.contains(service));
394     }
395   }
396 
397   /**
398    * This is for covering a case where the ServiceManager would behave strangely if constructed with
399    * no service under management. Listeners would never fire because the ServiceManager was healthy
400    * and stopped at the same time. This test ensures that listeners fire and isHealthy makes sense.
401    */
testEmptyServiceManager()402   public void testEmptyServiceManager() {
403     Logger logger = Logger.getLogger(ServiceManager.class.getName());
404     logger.setLevel(Level.FINEST);
405     TestLogHandler logHandler = new TestLogHandler();
406     logger.addHandler(logHandler);
407     ServiceManager manager = new ServiceManager(Arrays.<Service>asList());
408     RecordingListener listener = new RecordingListener();
409     manager.addListener(listener, directExecutor());
410     manager.startAsync().awaitHealthy();
411     assertTrue(manager.isHealthy());
412     assertTrue(listener.healthyCalled);
413     assertFalse(listener.stoppedCalled);
414     assertTrue(listener.failedServices.isEmpty());
415     manager.stopAsync().awaitStopped();
416     assertFalse(manager.isHealthy());
417     assertTrue(listener.stoppedCalled);
418     assertTrue(listener.failedServices.isEmpty());
419     // check that our NoOpService is not directly observable via any of the inspection methods or
420     // via logging.
421     assertEquals("ServiceManager{services=[]}", manager.toString());
422     assertTrue(manager.servicesByState().isEmpty());
423     assertTrue(manager.startupTimes().isEmpty());
424     Formatter logFormatter =
425         new Formatter() {
426           @Override
427           public String format(LogRecord record) {
428             return formatMessage(record);
429           }
430         };
431     for (LogRecord record : logHandler.getStoredLogRecords()) {
432       assertThat(logFormatter.format(record)).doesNotContain("NoOpService");
433     }
434   }
435 
436   /**
437    * Tests that a ServiceManager can be fully shut down if one of its failure listeners is slow or
438    * even permanently blocked.
439    */
testListenerDeadlock()440   public void testListenerDeadlock() throws InterruptedException {
441     final CountDownLatch failEnter = new CountDownLatch(1);
442     final CountDownLatch failLeave = new CountDownLatch(1);
443     final CountDownLatch afterStarted = new CountDownLatch(1);
444     Service failRunService =
445         new AbstractService() {
446           @Override
447           protected void doStart() {
448             new Thread() {
449               @Override
450               public void run() {
451                 notifyStarted();
452                 // We need to wait for the main thread to leave the ServiceManager.startAsync call
453                 // to
454                 // ensure that the thread running the failure callbacks is not the main thread.
455                 Uninterruptibles.awaitUninterruptibly(afterStarted);
456                 notifyFailed(new Exception("boom"));
457               }
458             }.start();
459           }
460 
461           @Override
462           protected void doStop() {
463             notifyStopped();
464           }
465         };
466     final ServiceManager manager =
467         new ServiceManager(Arrays.asList(failRunService, new NoOpService()));
468     manager.addListener(
469         new ServiceManager.Listener() {
470           @Override
471           public void failure(Service service) {
472             failEnter.countDown();
473             // block until after the service manager is shutdown
474             Uninterruptibles.awaitUninterruptibly(failLeave);
475           }
476         },
477         directExecutor());
478     manager.startAsync();
479     afterStarted.countDown();
480     // We do not call awaitHealthy because, due to races, that method may throw an exception.  But
481     // we really just want to wait for the thread to be in the failure callback so we wait for that
482     // explicitly instead.
483     failEnter.await();
484     assertFalse("State should be updated before calling listeners", manager.isHealthy());
485     // now we want to stop the services.
486     Thread stoppingThread =
487         new Thread() {
488           @Override
489           public void run() {
490             manager.stopAsync().awaitStopped();
491           }
492         };
493     stoppingThread.start();
494     // this should be super fast since the only non-stopped service is a NoOpService
495     stoppingThread.join(1000);
496     assertFalse("stopAsync has deadlocked!.", stoppingThread.isAlive());
497     failLeave.countDown(); // release the background thread
498   }
499 
500   /**
501    * Catches a bug where when constructing a service manager failed, later interactions with the
502    * service could cause IllegalStateExceptions inside the partially constructed ServiceManager.
503    * This ISE wouldn't actually bubble up but would get logged by ExecutionQueue. This obfuscated
504    * the original error (which was not constructing ServiceManager correctly).
505    */
testPartiallyConstructedManager()506   public void testPartiallyConstructedManager() {
507     Logger logger = Logger.getLogger("global");
508     logger.setLevel(Level.FINEST);
509     TestLogHandler logHandler = new TestLogHandler();
510     logger.addHandler(logHandler);
511     NoOpService service = new NoOpService();
512     service.startAsync();
513     assertThrows(IllegalArgumentException.class, () -> new ServiceManager(Arrays.asList(service)));
514     service.stopAsync();
515     // Nothing was logged!
516     assertEquals(0, logHandler.getStoredLogRecords().size());
517   }
518 
testPartiallyConstructedManager_transitionAfterAddListenerBeforeStateIsReady()519   public void testPartiallyConstructedManager_transitionAfterAddListenerBeforeStateIsReady() {
520     // The implementation of this test is pretty sensitive to the implementation :( but we want to
521     // ensure that if weird things happen during construction then we get exceptions.
522     final NoOpService service1 = new NoOpService();
523     // This service will start service1 when addListener is called.  This simulates service1 being
524     // started asynchronously.
525     Service service2 =
526         new Service() {
527           final NoOpService delegate = new NoOpService();
528 
529           @Override
530           public final void addListener(Listener listener, Executor executor) {
531             service1.startAsync();
532             delegate.addListener(listener, executor);
533           }
534 
535           // Delegates from here on down
536           @Override
537           public final Service startAsync() {
538             return delegate.startAsync();
539           }
540 
541           @Override
542           public final Service stopAsync() {
543             return delegate.stopAsync();
544           }
545 
546           @Override
547           public final void awaitRunning() {
548             delegate.awaitRunning();
549           }
550 
551           @Override
552           public final void awaitRunning(long timeout, TimeUnit unit) throws TimeoutException {
553             delegate.awaitRunning(timeout, unit);
554           }
555 
556           @Override
557           public final void awaitTerminated() {
558             delegate.awaitTerminated();
559           }
560 
561           @Override
562           public final void awaitTerminated(long timeout, TimeUnit unit) throws TimeoutException {
563             delegate.awaitTerminated(timeout, unit);
564           }
565 
566           @Override
567           public final boolean isRunning() {
568             return delegate.isRunning();
569           }
570 
571           @Override
572           public final State state() {
573             return delegate.state();
574           }
575 
576           @Override
577           public final Throwable failureCause() {
578             return delegate.failureCause();
579           }
580         };
581     IllegalArgumentException expected =
582         assertThrows(
583             IllegalArgumentException.class,
584             () -> new ServiceManager(Arrays.asList(service1, service2)));
585     assertThat(expected.getMessage()).contains("started transitioning asynchronously");
586   }
587 
588   /**
589    * This test is for a case where two Service.Listener callbacks for the same service would call
590    * transitionService in the wrong order due to a race. Due to the fact that it is a race this test
591    * isn't guaranteed to expose the issue, but it is at least likely to become flaky if the race
592    * sneaks back in, and in this case flaky means something is definitely wrong.
593    *
594    * <p>Before the bug was fixed this test would fail at least 30% of the time.
595    */
testTransitionRace()596   public void testTransitionRace() throws TimeoutException {
597     for (int k = 0; k < 1000; k++) {
598       List<Service> services = Lists.newArrayList();
599       for (int i = 0; i < 5; i++) {
600         services.add(new SnappyShutdownService(i));
601       }
602       ServiceManager manager = new ServiceManager(services);
603       manager.startAsync().awaitHealthy();
604       manager.stopAsync().awaitStopped(10, TimeUnit.SECONDS);
605     }
606   }
607 
608   /**
609    * This service will shut down very quickly after stopAsync is called and uses a background thread
610    * so that we know that the stopping() listeners will execute on a different thread than the
611    * terminated() listeners.
612    */
613   private static class SnappyShutdownService extends AbstractExecutionThreadService {
614     final int index;
615     final CountDownLatch latch = new CountDownLatch(1);
616 
SnappyShutdownService(int index)617     SnappyShutdownService(int index) {
618       this.index = index;
619     }
620 
621     @Override
run()622     protected void run() throws Exception {
623       latch.await();
624     }
625 
626     @Override
triggerShutdown()627     protected void triggerShutdown() {
628       latch.countDown();
629     }
630 
631     @Override
serviceName()632     protected String serviceName() {
633       return this.getClass().getSimpleName() + "[" + index + "]";
634     }
635   }
636 
testNulls()637   public void testNulls() {
638     ServiceManager manager = new ServiceManager(Arrays.<Service>asList());
639     new NullPointerTester()
640         .setDefault(ServiceManager.Listener.class, new RecordingListener())
641         .testAllPublicInstanceMethods(manager);
642   }
643 
644   private static final class RecordingListener extends ServiceManager.Listener {
645     volatile boolean healthyCalled;
646     volatile boolean stoppedCalled;
647     final Set<Service> failedServices = Sets.newConcurrentHashSet();
648 
649     @Override
healthy()650     public void healthy() {
651       healthyCalled = true;
652     }
653 
654     @Override
stopped()655     public void stopped() {
656       stoppedCalled = true;
657     }
658 
659     @Override
failure(Service service)660     public void failure(Service service) {
661       failedServices.add(service);
662     }
663   }
664 
isWindows()665   private static boolean isWindows() {
666     return OS_NAME.value().startsWith("Windows");
667   }
668 
isJava8()669   private static boolean isJava8() {
670     return JAVA_SPECIFICATION_VERSION.value().equals("1.8");
671   }
672 }
673