View Javadoc
1   /*
2    * Copyright 2019-2023 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.util;
6   
7   import static org.hamcrest.MatcherAssert.assertThat;
8   import static org.hamcrest.Matchers.greaterThan;
9   import static org.hamcrest.Matchers.is;
10  import static org.hamcrest.Matchers.lessThan;
11  import static org.hamcrest.Matchers.lessThanOrEqualTo;
12  import static org.hamcrest.Matchers.not;
13  import static org.hamcrest.Matchers.notNullValue;
14  import static oshi.util.Memoizer.memoize;
15  
16  import java.util.ArrayList;
17  import java.util.Collection;
18  import java.util.Locale;
19  import java.util.Random;
20  import java.util.concurrent.ExecutionException;
21  import java.util.concurrent.ExecutorService;
22  import java.util.concurrent.Future;
23  import java.util.concurrent.LinkedBlockingQueue;
24  import java.util.concurrent.ThreadPoolExecutor;
25  import java.util.concurrent.TimeUnit;
26  import java.util.function.Supplier;
27  
28  import org.junit.jupiter.api.AfterEach;
29  import org.junit.jupiter.api.BeforeEach;
30  import org.junit.jupiter.api.Test;
31  import org.junit.jupiter.api.condition.DisabledOnOs;
32  import org.junit.jupiter.api.condition.OS;
33  
34  @DisabledOnOs(OS.SOLARIS)
35  final class MemoizerTest {
36      // We want enough threads that some of them are forced to wait
37      private static final int numberOfThreads = Math.max(5, Runtime.getRuntime().availableProcessors() + 2);
38  
39      private ExecutorService ex;
40  
41      @BeforeEach
42      void before() {
43          final ThreadPoolExecutor ex = new ThreadPoolExecutor(numberOfThreads, numberOfThreads, 0L,
44                  TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
45          ex.allowCoreThreadTimeOut(false);
46          ex.prestartAllCoreThreads(); // make sure we don't lose refreshes in tests because of
47                                       // spending time to start threads
48          this.ex = ex;
49      }
50  
51      @AfterEach
52      void after() throws InterruptedException {
53          ex.shutdownNow();
54          ex.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
55      }
56  
57      @Test
58      void get() throws Throwable {
59          // With max time limits these tests take a minute to run. But with no changes to
60          // the memoizer it's simply testing overkill. Use a RNG to limit these tests
61          // regularly but occasionally run the longer tests.
62          int x = new Random().nextInt(50);
63          // Set minimal defaults
64          int refreshIters = 200; // ~ 2 seconds
65          int noRefreshIters = 2_000; // ~ 2 seconds
66          if (x == 0) {
67              // 2% chance full length noRefresh
68              noRefreshIters = 20_000; // ~ 20 seconds
69          } else if (x == 1) {
70              // 2% chance full length refresh
71              refreshIters = 4000; // ~ 40 seconds
72          } else if (x < 12) {
73              // 20% chance longer noRerfesh
74              noRefreshIters = 5_000; // ~ 5 seconds
75          } else if (x < 22) {
76              // 20% chance longer rerfesh
77              refreshIters = 500; // ~ 5 seconds
78          }
79          // Do tests with no refresh.
80          long iterationDurationNanos = TimeUnit.MILLISECONDS.toNanos(1);
81          long ttlNanos = -1;
82          for (int r = 0; r < noRefreshIters; r++) {
83              run(iterationDurationNanos, ttlNanos);
84          }
85          // Do tests with refresh after 0.1 ms.
86          iterationDurationNanos = TimeUnit.MILLISECONDS.toNanos(10);
87          ttlNanos = iterationDurationNanos / 100;
88          assertThat("ttlNanos should not be zero", ttlNanos, is(not(0L))); // avoid div/0 later
89          for (int r = 0; r < refreshIters; r++) {
90              run(iterationDurationNanos, ttlNanos);
91          }
92      }
93  
94      private void run(final long iterationDurationNanos, final long ttlNanos) throws Throwable {
95          final Supplier<Long> s = new Supplier<Long>() {
96              private long value;
97  
98              // this method is not thread-safe, the returned value of the counter may go down
99              // if this method is called concurrently from different threads
100             @Override
101             public Long get() {
102                 return ++value;
103             }
104         };
105         // The memoizer we are testing
106         final Supplier<Long> m = memoize(s, ttlNanos);
107         // Hold the results until all threads terminate
108         final Collection<Future<Void>> results = new ArrayList<>();
109         // Mark the start time, end after iterationDuration
110         final long beginNanos = System.nanoTime();
111         for (int tid = 0; tid < numberOfThreads; tid++) {
112             results.add(ex.submit(() -> {
113                 // First read from the memoizer. Only one thread will win this race to increment
114                 // 0 to 1, but all threads should read at least 1, if not increment further
115                 Long previousValue = m.get();
116                 assertThat("previousValue should not be null", previousValue, is(notNullValue()));
117                 assertThat("previousValue should be greater than zero", previousValue, is(greaterThan(0L)));
118                 // Memoizer's ttl was set during previous call (for race winning thread) or
119                 // earlier (for losing threads) but if we delay for at least ttl from now, we
120                 // are sure to get at least one increment if ttl is nonnegative
121                 final long firstSupplierCallNanos = System.nanoTime();
122                 // using guaranteedIteration this loop is guaranteed to be executed at
123                 // least once regardless of whether we have exceeded time delays
124                 boolean guaranteedIteration = false;
125                 long now;
126                 while ((now = System.nanoTime()) - beginNanos < iterationDurationNanos
127                         || now - firstSupplierCallNanos < ttlNanos || (guaranteedIteration = !guaranteedIteration)) {
128                     // guaranteedIteration will only be set true when the first two timing
129                     // conditions are false, which will allow at least one iteration. After that
130                     // final iteration the boolean will toggle false again to stop the loop.
131                     if (Thread.currentThread().isInterrupted()) {
132                         throw new InterruptedException();
133                     }
134                     final Long newValue = m.get();
135                     // check that we never get uninitialized
136                     assertThat("newValue should not be null", newValue, is(notNullValue()));
137                     // check that the counter never goes down // value
138                     assertThat("newValue shuld be larger", newValue, is(not(lessThan(previousValue))));
139                     previousValue = newValue;
140                 }
141                 return null;
142             }));
143         }
144         /*
145          * Make sure all the submitted tasks finished correctly
146          */
147         finishAllThreads(results);
148         /*
149          * All the writes to s.value field happened-before this read because of all the result.get() invocations, so it
150          * holds the final/max value returned by any thread. We cannot access s.value but it's private, and s.get() will
151          * increment before returning, so here we subtract 1 from the result to determine what the internal s.value was
152          * before this call increments it.
153          */
154         final long actualNumberOfIncrements = s.get() - 1;
155         testIncrementCounts(actualNumberOfIncrements, iterationDurationNanos, ttlNanos);
156 
157     }
158 
159     private static void finishAllThreads(Collection<Future<Void>> results)
160             throws InterruptedException, ExecutionException {
161         for (final Future<Void> result : results) {
162             result.get();
163         }
164     }
165 
166     private static void testIncrementCounts(long actualNumberOfIncrements, long iterationDurationNanos, long ttlNanos) {
167         if (ttlNanos < 0) {
168             assertThat(String.format(Locale.ROOT, "ttlNanos=%d", ttlNanos), actualNumberOfIncrements, is(1L));
169         } else {
170             /*
171              * Calculation of expectedNumberOfIncrements is a bit tricky because there is no such thing. We can only
172              * talk about min and max possible values when ttl > 0.
173              *
174              * Min: Two increments are guaranteed: the initial one, because it does not depend on timings, and a second
175              * one which ensures at least ttlNanos have elapsed since the first one. All other refreshes may or may not
176              * happen depending on the timings. Therefore the min is 2. In the case of negative ttl we should get only
177              * one increment ever; otherwise we must have at least 2 increments.
178              *
179              * Max: Each thread has a chance to refresh one more time after (iterationDurationNanos / ttlNanos)
180              * refreshes have been collectively done, which will increment again. This happens because an arbitrary
181              * amount of time may elapse between the instant when a thread enters the while cycle body for the last time
182              * (the last iteration), and the instant that is observed by the MemoizedObject.get method. Additionally,
183              * each thread may refresh one more time because of the last iteration of the loop caused by
184              * guaranteedIteration. Therefore each thread may do up to 2 additional refreshes.
185              */
186             final long minExpectedNumberOfIncrements = 2L;
187             final long maxExpectedNumberOfIncrements = (iterationDurationNanos / ttlNanos) + 2L * numberOfThreads;
188 
189             assertThat(String.format(Locale.ROOT, "ttlNanos=%s", ttlNanos), minExpectedNumberOfIncrements,
190                     is(lessThanOrEqualTo(actualNumberOfIncrements)));
191             assertThat(String.format(Locale.ROOT, "ttlNanos=%s", ttlNanos), actualNumberOfIncrements,
192                     is(lessThanOrEqualTo(maxExpectedNumberOfIncrements)));
193         }
194     }
195 }