• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

talsma-ict / context-propagation / #1516

06 Nov 2024 01:38PM CUT coverage: 83.968% (-7.8%) from 91.752%
#1516

push

web-flow
Merge 5d3025a76 into eef10dfb8

115 of 171 new or added lines in 25 files covered. (67.25%)

2 existing lines in 1 file now uncovered.

948 of 1129 relevant lines covered (83.97%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.17
/context-propagation-api/src/main/java/nl/talsmasoftware/context/core/ContextManagers.java
1
/*
2
 * Copyright 2016-2024 Talsma ICT
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
package nl.talsmasoftware.context.core;
17

18
import nl.talsmasoftware.context.api.Context;
19
import nl.talsmasoftware.context.api.ContextManager;
20
import nl.talsmasoftware.context.api.ContextObserver;
21
import nl.talsmasoftware.context.api.ContextSnapshot;
22
import nl.talsmasoftware.context.api.ContextSnapshot.Reactivation;
23
import nl.talsmasoftware.context.core.delegation.Wrapper;
24

25
import java.util.ArrayList;
26
import java.util.Arrays;
27
import java.util.Iterator;
28
import java.util.LinkedList;
29
import java.util.List;
30
import java.util.ServiceLoader;
31
import java.util.concurrent.CopyOnWriteArrayList;
32
import java.util.logging.Level;
33
import java.util.logging.Logger;
34

35
/**
36
 * Core implementation to allow {@link #createContextSnapshot() taking a snapshot of all contexts}.
37
 *
38
 * <p>
39
 * Such a {@link ContextSnapshot snapshot} can be passed to a background task to allow the context to be
40
 * {@link ContextSnapshot#reactivate() reactivated} in that background thread, until it gets
41
 * {@link Context#close() closed} again (preferably in a <code>try-with-resources</code> construct).
42
 *
43
 * @author Sjoerd Talsma
44
 * @since 1.1.0
45
 */
46
public final class ContextManagers {
47
    private static final Logger LOGGER = Logger.getLogger(ContextManagers.class.getName());
1✔
48

49
    /**
50
     * Registered observers.
51
     */
52
    private static final CopyOnWriteArrayList<ObservableContextManager> OBSERVERS =
1✔
53
            new CopyOnWriteArrayList<>();
54

55
    /**
56
     * Sometimes a single, fixed classloader may be necessary (e.g. #97)
57
     */
58
    private static volatile ClassLoader classLoaderOverride = null;
1✔
59

60
    /**
61
     * Private constructor to avoid instantiation of this class.
62
     */
63
    private ContextManagers() {
1✔
64
        throw new UnsupportedOperationException();
1✔
65
    }
66

67
    /**
68
     * This method is able to create a 'snapshot' from the current
69
     * {@link ContextManager#getActiveContextValue() active context value}
70
     * from <em>all known {@link ContextManager}</em> implementations.
71
     *
72
     * <p>
73
     * This snapshot is returned as a single object that can be temporarily
74
     * {@link ContextSnapshot#reactivate() reactivated}. Don't forget to {@link Context#close() close} the reactivated
75
     * context once you're done, preferably in a <code>try-with-resources</code> construct.
76
     *
77
     * @return A new snapshot that can be reactivated elsewhere (e.g. a background thread)
78
     * within a try-with-resources construct.
79
     */
80
    public static nl.talsmasoftware.context.api.ContextSnapshot createContextSnapshot() {
81
        final long start = System.nanoTime();
1✔
82
        final List<ContextManager<?>> managers = new LinkedList<>();
1✔
83
        final List<Object> values = new LinkedList<>();
1✔
84
        Long managerStart = null;
1✔
85
        for (ContextManager<?> manager : getContextManagers()) {
1✔
86
            managerStart = System.nanoTime();
1✔
87
            try {
88
                final Object activeContextValue = manager.getActiveContextValue();
1✔
89
                if (activeContextValue != null) {
1✔
90
                    values.add(activeContextValue);
1✔
91
                    managers.add(manager);
1✔
92
                    if (LOGGER.isLoggable(Level.FINEST)) {
1✔
NEW
93
                        LOGGER.finest("Active context value of " + manager + " added to new snapshot: " + activeContextValue);
×
94
                    }
95
                    Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "getActiveContext");
1✔
96
                } else if (LOGGER.isLoggable(Level.FINEST)) {
1✔
97
                    LOGGER.log(Level.FINEST, "There is no active context for " + manager + " in this snapshot.");
×
98
                }
99
            } catch (RuntimeException rte) {
1✔
100
                LOGGER.log(Level.WARNING, "Exception obtaining active context from " + manager + " for snapshot.", rte);
1✔
101
                Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "getActiveContext.exception");
1✔
102
            }
1✔
103
        }
1✔
104
        if (managerStart == null) {
1✔
105
            NoContextManagersFound noContextManagersFound = new NoContextManagersFound();
1✔
106
            LOGGER.log(Level.INFO, noContextManagersFound.getMessage(), noContextManagersFound);
1✔
107
        }
108
        ContextSnapshotImpl result = new ContextSnapshotImpl(managers, values);
1✔
109
        Timers.timed(System.nanoTime() - start, ContextManagers.class, "createContextSnapshot");
1✔
110
        return result;
1✔
111
    }
112

113
    /**
114
     * Clears all active contexts from the current thread.
115
     *
116
     * <p>
117
     * Contexts that are 'stacked' (i.e. restore the previous state upon close) should be
118
     * closed in a way that includes all 'parent' contexts as well.
119
     *
120
     * <p>
121
     * This operation is not intended to be used by general application code as it likely breaks any 'stacked'
122
     * active context that surrounding code may depend upon.
123
     * Appropriate use includes thread management, where threads are reused by some pooling
124
     * mechanism. For example, it is considered safe to clear the context when obtaining a 'fresh' thread from a
125
     * thread pool (as no context expectations should exist at that point).
126
     * An even better strategy would be to clear the context right before returning a used thread to the pool
127
     * as this will allow any unclosed contexts to be garbage collected. Besides preventing contextual issues,
128
     * this reduces the risk of memory leaks by unbalanced context calls.
129
     */
130
    public static void clearActiveContexts() {
131
        final long start = System.nanoTime();
1✔
132
        Long managerStart = null;
1✔
133
        for (ContextManager<?> manager : getContextManagers()) {
1✔
134
            managerStart = System.nanoTime();
1✔
135
            try {
136
                manager.clear();
1✔
137
                if (LOGGER.isLoggable(Level.FINEST)) {
1✔
NEW
138
                    LOGGER.finest("Active context of " + manager + " was cleared.");
×
139
                }
140
                Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "clear");
1✔
141
            } catch (RuntimeException rte) {
×
142
                LOGGER.log(Level.WARNING, "Exception clearing active context from " + manager + ".", rte);
×
143
                Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "clear.exception");
×
144
            }
1✔
145
        }
1✔
146
        if (managerStart == null) {
1✔
147
            NoContextManagersFound noContextManagersFound = new NoContextManagersFound();
1✔
148
            LOGGER.log(Level.INFO, noContextManagersFound.getMessage(), noContextManagersFound);
1✔
149
        }
150
        Timers.timed(System.nanoTime() - start, ContextManagers.class, "clearActiveContexts");
1✔
151
    }
1✔
152

153
    /**
154
     * Register an observer for contexts managed by the specified ContextManager type.
155
     *
156
     * @param contextObserver            The observer to register.
157
     * @param observedContextManagerType The context manager type to observe.
158
     * @param <T>                        Type of the value in the context.
159
     * @return {@code true} if the observer was registered.
160
     * @since 1.1.0
161
     */
162
    public static <T> boolean registerContextObserver(ContextObserver<? super T> contextObserver, Class<? extends ContextManager<T>> observedContextManagerType) {
163
        if (contextObserver == null) {
×
164
            throw new NullPointerException("Context observer must not be null.");
×
165
        } else if (observedContextManagerType == null) {
×
166
            throw new NullPointerException("Observed ContextManager type must not be null.");
×
167
        }
168

169
        // Find ContextManager to register.
NEW
170
        ObservableContextManager<T> observableContextManager = null;
×
NEW
171
        ContextManager<T> contextManager = null;
×
NEW
172
        for (ContextManager<?> manager : getContextManagers()) {
×
173
            if (manager instanceof ObservableContextManager
×
NEW
174
                    && ((ObservableContextManager<?>) manager).observes(observedContextManagerType)) {
×
NEW
175
                observableContextManager = (ObservableContextManager<T>) manager;
×
176
                break;
×
177
            } else if (observedContextManagerType.isInstance(manager)) {
×
NEW
178
                contextManager = (ContextManager<T>) manager;
×
179
                break;
×
180
            }
181
        }
×
182
        if (observableContextManager == null && contextManager == null) {
×
183
            LOGGER.warning("Trying to register observer to missing ContextManager type: " + observedContextManagerType + ".");
×
184
            return false;
×
185
        }
186

187
        if (observableContextManager == null) {
×
188
            // Register new observer by wrapping the context manager.
189
            ObservableContextManager<T> newObserver = new ObservableContextManager<T>(contextManager, (List) Arrays.asList(contextObserver));
×
190
            if (OBSERVERS.addIfAbsent(newObserver)) {
×
191
                return true;
×
192
            }
193

194
            // There is already an existing ObservableContextManager, add the observer to it.
195
            observableContextManager = OBSERVERS.get(OBSERVERS.indexOf(newObserver));
×
196
        }
197

198
        // Add the context observer to the existing observable context manager.
199
        synchronized (observableContextManager) {
×
200
            return observableContextManager.observers.addIfAbsent(contextObserver);
×
201
        }
202
    }
203

204
    /**
205
     * Unregister an observer for any context.
206
     *
207
     * @param contextObserver The previously registered context observer.
208
     * @return {@code true} if the observer was unregistered.
209
     * @since 1.1.0
210
     */
211
    public static boolean unregisterContextObserver(ContextObserver<?> contextObserver) {
212
        boolean unregistered = false;
×
NEW
213
        for (ObservableContextManager<?> observer : OBSERVERS) {
×
214
            unregistered |= observer.observers.remove(contextObserver);
×
215
            synchronized (observer) {
×
216
                if (observer.observers.isEmpty()) {
×
217
                    OBSERVERS.remove(observer);
×
218
                }
219
            }
×
220
        }
×
221
        return unregistered;
×
222
    }
223

224
    /**
225
     * Override the {@linkplain ClassLoader} used to lookup {@linkplain ContextManager contextmanagers}.
226
     * <p>
227
     * Normally, taking a snapshot uses the {@linkplain Thread#getContextClassLoader() Context ClassLoader} from the
228
     * {@linkplain Thread#currentThread() current thread} to look up all {@linkplain ContextManager context managers}.
229
     * It is possible to configure a fixed, single classloader in your application for looking up the context managers.
230
     * <p>
231
     * Using this method to specify a fixed classloader will only impact
232
     * new {@linkplain ContextSnapshot context snapshots}. Existing snapshots will not be impacted.
233
     * <p>
234
     * <strong>Notes:</strong><br>
235
     * <ul>
236
     * <li>Please be aware that this configuration is global!
237
     * <li>This will also affect the lookup of
238
     * {@linkplain nl.talsmasoftware.context.api.ContextObserver context observers}
239
     * </ul>
240
     *
241
     * @param classLoader The single, fixed ClassLoader to use for finding context managers.
242
     *                    Specify {@code null} to restore the default behaviour.
243
     * @since 1.0.5
244
     */
245
    public static synchronized void useClassLoader(ClassLoader classLoader) {
246
        if (classLoaderOverride == classLoader) {
1✔
247
            LOGGER.finest(() -> "Maintaining classloader override as " + classLoader + " (unchanged)");
1✔
248
            return;
1✔
249
        }
250
        LOGGER.fine(() -> "Updating classloader override to " + classLoader + " (was: " + classLoaderOverride + ")");
1✔
251
        classLoaderOverride = classLoader;
1✔
252
    }
1✔
253

254
    private static Iterable<ContextManager> getContextManagers() {
255
        // TODO change to stream implementation when java 8
256
        final Iterable<ContextManager> resolved = classLoaderOverride == null ? ServiceLoader.load(ContextManager.class)
1✔
257
                : ServiceLoader.load(ContextManager.class, classLoaderOverride);
1✔
258
        return OBSERVERS.isEmpty() ? resolved : new Iterable<ContextManager>() {
1✔
259
            public Iterator<ContextManager> iterator() {
260
                return new Iterator<ContextManager>() {
×
NEW
261
                    private final Iterator<ContextManager> delegate = resolved.iterator();
×
262

263
                    public boolean hasNext() {
264
                        return delegate.hasNext();
×
265
                    }
266

267
                    public ContextManager next() {
268
                        ContextManager contextManager = delegate.next();
×
269
                        if (!(contextManager instanceof ObservableContextManager)) {
×
270
                            for (ObservableContextManager observableContextManager : OBSERVERS) {
×
271
                                if (observableContextManager.isWrapperOf(contextManager)) {
×
272
                                    return observableContextManager;
×
273
                                }
274
                            }
×
275
                        }
276
                        return contextManager;
×
277
                    }
278

279
                    public void remove() {
280
                        delegate.remove();
×
281
                    }
×
282
                };
283
            }
284
        };
285
    }
286

287
    /**
288
     * Implementation of the <code>createContextSnapshot</code> functionality that can reactivate all values from the
289
     * snapshot in each corresponding {@link ContextManager}.
290
     */
291
    private static final class ContextSnapshotImpl implements nl.talsmasoftware.context.api.ContextSnapshot {
292
        private static final ContextManager[] MANAGER_ARRAY = new ContextManager[0];
1✔
293
        private final ContextManager[] managers;
294
        private final Object[] values;
295

296
        private ContextSnapshotImpl(List<ContextManager<?>> managers, List<Object> values) {
1✔
297
            this.managers = managers.toArray(MANAGER_ARRAY);
1✔
298
            this.values = values.toArray();
1✔
299
        }
1✔
300

301
        public Reactivation reactivate() {
302
            final long start = System.nanoTime();
1✔
303
            final List<Context<?>> reactivatedContexts = new ArrayList<Context<?>>(managers.length);
1✔
304
            try {
305
                for (int i = 0; i < managers.length && i < values.length; i++) {
1✔
306
                    reactivatedContexts.add(reactivate(managers[i], values[i]));
1✔
307
                }
308
                ReactivationImpl reactivation = new ReactivationImpl(reactivatedContexts);
1✔
309
                Timers.timed(System.nanoTime() - start, nl.talsmasoftware.context.api.ContextSnapshot.class, "reactivate");
1✔
310
                return reactivation;
1✔
311
            } catch (RuntimeException reactivationException) {
1✔
312
                for (Context alreadyReactivated : reactivatedContexts) {
1✔
313
                    if (alreadyReactivated != null) try {
1✔
314
                        if (LOGGER.isLoggable(Level.FINEST)) {
1✔
315
                            LOGGER.finest("Snapshot reactivation failed! " +
×
316
                                    "Closing already reactivated context: " + alreadyReactivated + ".");
317
                        }
318
                        alreadyReactivated.close();
1✔
319
                    } catch (RuntimeException rte) {
×
NEW
320
                        reactivationException.addSuppressed(rte);
×
321
                    }
1✔
322
                }
1✔
323
                throw reactivationException;
1✔
324
            }
325
        }
326

327
        @SuppressWarnings("unchecked") // As we got the values from the managers themselves, they must also accept them!
328
        private Context reactivate(ContextManager contextManager, Object snapshotValue) {
329
            long start = System.nanoTime();
1✔
330
            Context reactivated = contextManager.initializeNewContext(snapshotValue);
1✔
331
            if (LOGGER.isLoggable(Level.FINEST)) {
1✔
332
                LOGGER.finest("Context reactivated from snapshot by " + contextManager + ": " + reactivated + ".");
×
333
            }
334
            Timers.timed(System.nanoTime() - start, contextManager.getClass(), "initializeNewContext");
1✔
335
            return reactivated;
1✔
336
        }
337

338
        @Override
339
        public String toString() {
340
            return "ContextSnapshot{size=" + managers.length + '}';
1✔
341
        }
342
    }
343

344
    /**
345
     * Implementation of the reactivated 'container' context that closes all reactivated contexts
346
     * when it is closed itself.<br>
347
     * This context contains no meaningful value in itself and purely exists to close the reactivated contexts.
348
     */
349
    private static final class ReactivationImpl implements Reactivation {
350
        private final List<Context<?>> reactivated;
351

352
        private ReactivationImpl(List<Context<?>> reactivated) {
1✔
353
            this.reactivated = reactivated;
1✔
354
        }
1✔
355

356
        public void close() {
357
            RuntimeException closeException = null;
1✔
358
            // close in reverse order of reactivation
359
            for (int i = this.reactivated.size() - 1; i >= 0; i--) {
1✔
360
                Context<?> reactivated = this.reactivated.get(i);
1✔
361
                if (reactivated != null) try {
1✔
362
                    reactivated.close();
1✔
363
                } catch (RuntimeException rte) {
1✔
364
                    if (closeException == null) closeException = rte;
1✔
NEW
365
                    else closeException.addSuppressed(rte);
×
366
                }
1✔
367
            }
368
            if (closeException != null) throw closeException;
1✔
369
        }
1✔
370

371
        @Override
372
        public String toString() {
373
            return "ReactivatedContext{size=" + reactivated.size() + '}';
1✔
374
        }
375
    }
376

377
    private static final class ObservableContextManager<T> extends Wrapper<ContextManager<T>> implements ContextManager<T> {
378
        private final CopyOnWriteArrayList<ContextObserver<? super T>> observers;
379

380
        private ObservableContextManager(ContextManager<T> delegate, List<ContextObserver<? super T>> observers) {
381
            super(delegate);
×
NEW
382
            this.observers = new CopyOnWriteArrayList<>(observers);
×
383
        }
×
384

385
        private boolean observes(Class<? extends ContextManager<?>> contextManagerType) {
386
            return contextManagerType.isInstance(delegate());
×
387
        }
388

389
        @Override
390
        public T getActiveContextValue() {
NEW
391
            return delegate().getActiveContextValue();
×
392
        }
393

394
        @Override
395
        public void clear() {
NEW
396
            delegate().clear();
×
397
        }
×
398

399
        private void notifyActivated(T newValue, T oldValue) {
400
            for (ContextObserver<? super T> observer : observers) {
×
401
                try {
402
                    observer.onActivate(newValue, oldValue);
×
403
                } catch (RuntimeException observerError) {
×
404
                    LOGGER.log(Level.SEVERE, "Error in observer.onActivate of " + observer, observerError);
×
405
                }
×
406
            }
×
407
        }
×
408

409
        private void notifyDeactivated(T deactivatedValue, T restoredValue) {
410
            for (ContextObserver<? super T> observer : observers) {
×
411
                try {
412
                    observer.onDeactivate(deactivatedValue, restoredValue);
×
413
                } catch (RuntimeException observerError) {
×
414
                    LOGGER.log(Level.SEVERE, "Error in observer.onActivate of " + observer, observerError);
×
415
                }
×
416
            }
×
417
        }
×
418

419
        @Override
420
        public Context<T> initializeNewContext(final T newValue) {
421
            final T oldValue = getActiveContextValue();
×
422
            final Context<T> context = delegate().initializeNewContext(newValue);
×
423
            notifyActivated(newValue, oldValue);
×
424

425
            return new Context<T>() {
×
426
                public T getValue() {
427
                    return context.getValue();
×
428
                }
429

430
                public void close() {
431
                    T deactivated = context.getValue(); // get before closing!
×
432
                    context.close();
×
433
                    notifyDeactivated(deactivated, getActiveContextValue());
×
434
                }
×
435
            };
436
        }
437

438
        @Override
439
        public String toString() {
440
            return getClass().getSimpleName() + '{' + delegate() + ", " + observers + '}';
×
441
        }
442
    }
443

444
    /**
445
     * Exception that we don't actually throw, but it helps track the issue if we log it including the stacktrace.
446
     */
447
    private static class NoContextManagersFound extends RuntimeException {
448
        private NoContextManagersFound() {
449
            super("Context snapshot was created but no ContextManagers were found!"
1✔
450
                    + " Thread=" + Thread.currentThread()
1✔
451
                    + ", ContextClassLoader=" + Thread.currentThread().getContextClassLoader());
1✔
452
        }
1✔
453
    }
454
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc