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

talsma-ict / context-propagation / #1537

08 Nov 2024 09:58AM UTC coverage: 91.953% (+0.3%) from 91.679%
#1537

push

web-flow
Merge 4a83fa71b into 72abd1122

271 of 327 new or added lines in 26 files covered. (82.87%)

857 of 932 relevant lines covered (91.95%)

0.92 hits per line

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

90.55
/context-propagation-core/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.ContextSnapshot;
21
import nl.talsmasoftware.context.api.ContextSnapshot.Reactivation;
22
import nl.talsmasoftware.context.api.ContextTimer;
23

24
import java.util.ArrayList;
25
import java.util.Collections;
26
import java.util.LinkedList;
27
import java.util.List;
28
import java.util.ServiceLoader;
29
import java.util.logging.Level;
30
import java.util.logging.Logger;
31

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

46
    /**
47
     * Sometimes a single, fixed classloader may be necessary (e.g. #97)
48
     */
49
    private static volatile ClassLoader classLoaderOverride = null;
1✔
50

51
    private static volatile List<ContextManager<?>> contextManagers = null;
1✔
52

53
    private static volatile List<ContextTimer> contextTimers = null;
1✔
54

55
    /**
56
     * Private constructor to avoid instantiation of this class.
57
     */
58
    private ContextManagers() {
1✔
59
        throw new UnsupportedOperationException();
1✔
60
    }
61

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

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

149
    /**
150
     * Override the {@linkplain ClassLoader} used to lookup {@linkplain ContextManager contextmanagers}.
151
     * <p>
152
     * Normally, taking a snapshot uses the {@linkplain Thread#getContextClassLoader() Context ClassLoader} from the
153
     * {@linkplain Thread#currentThread() current thread} to look up all {@linkplain ContextManager context managers}.
154
     * It is possible to configure a fixed, single classloader in your application for looking up the context managers.
155
     * <p>
156
     * Using this method to specify a fixed classloader will only impact
157
     * new {@linkplain ContextSnapshot context snapshots}. Existing snapshots will not be impacted.
158
     * <p>
159
     * <strong>Notes:</strong><br>
160
     * <ul>
161
     * <li>Please be aware that this configuration is global!
162
     * <li>This will also affect the lookup of
163
     * {@linkplain nl.talsmasoftware.context.api.ContextTimer context timers}
164
     * </ul>
165
     *
166
     * @param classLoader The single, fixed ClassLoader to use for finding context managers.
167
     *                    Specify {@code null} to restore the default behaviour.
168
     * @since 1.0.5
169
     */
170
    public static synchronized void useClassLoader(ClassLoader classLoader) {
171
        if (classLoaderOverride == classLoader) {
1✔
172
            LOGGER.finest(() -> "Maintaining classloader override as " + classLoader + " (unchanged)");
1✔
173
            return;
1✔
174
        }
175
        LOGGER.fine(() -> "Updating classloader override to " + classLoader + " (was: " + classLoaderOverride + ")");
1✔
176
        classLoaderOverride = classLoader;
1✔
177
        contextManagers = null;
1✔
178
        contextTimers = null;
1✔
179
    }
1✔
180

181
    @SuppressWarnings({"unchecked", "rawtypes"})
182
    private static List<ContextManager<?>> getContextManagers() {
183
        if (contextManagers == null) {
1✔
184
            synchronized (ContextManagers.class) {
1✔
185
                if (contextManagers == null) {
1✔
186
                    contextManagers = (List) load(ContextManager.class);
1✔
187
                }
188
            }
1✔
189
        }
190
        return contextManagers;
1✔
191
    }
192

193
    static List<ContextTimer> getContextTimers() {
194
        if (contextTimers == null) {
1✔
195
            synchronized (ContextManagers.class) {
1✔
196
                if (contextTimers == null) {
1✔
197
                    contextTimers = load(ContextTimer.class);
1✔
198
                }
199
            }
1✔
200
        }
201
        return contextTimers;
1✔
202
    }
203

204
    private static <T> List<T> load(Class<T> type) {
205
        ArrayList<T> list = new ArrayList<>();
1✔
206
        if (classLoaderOverride == null) {
1✔
207
            ServiceLoader.load(type).forEach(list::add);
1✔
208
        } else {
209
            ServiceLoader.load(type, classLoaderOverride).forEach(list::add);
1✔
210
        }
211
        list.trimToSize();
1✔
212
        return Collections.unmodifiableList(list);
1✔
213
    }
214

215
    /**
216
     * Implementation of the <code>createContextSnapshot</code> functionality that can reactivate all values from the
217
     * snapshot in each corresponding {@link ContextManager}.
218
     */
219
    @SuppressWarnings("rawtypes")
220
    private static final class ContextSnapshotImpl implements nl.talsmasoftware.context.api.ContextSnapshot {
221
        private static final ContextManager[] MANAGER_ARRAY = new ContextManager[0];
1✔
222
        private final ContextManager[] managers;
223
        private final Object[] values;
224

225
        private ContextSnapshotImpl(List<ContextManager<?>> managers, List<Object> values) {
1✔
226
            this.managers = managers.toArray(MANAGER_ARRAY);
1✔
227
            this.values = values.toArray();
1✔
228
        }
1✔
229

230
        public Reactivation reactivate() {
231
            final long start = System.nanoTime();
1✔
232
            final List<Context<?>> reactivatedContexts = new ArrayList<Context<?>>(managers.length);
1✔
233
            try {
234
                for (int i = 0; i < managers.length && i < values.length; i++) {
1✔
235
                    reactivatedContexts.add(reactivate(managers[i], values[i]));
1✔
236
                }
237
                ReactivationImpl reactivation = new ReactivationImpl(reactivatedContexts);
1✔
238
                Timers.timed(System.nanoTime() - start, nl.talsmasoftware.context.api.ContextSnapshot.class, "reactivate");
1✔
239
                return reactivation;
1✔
240
            } catch (RuntimeException reactivationException) {
1✔
241
                for (Context alreadyReactivated : reactivatedContexts) {
1✔
242
                    if (alreadyReactivated != null) try {
1✔
243
                        if (LOGGER.isLoggable(Level.FINEST)) {
1✔
NEW
244
                            LOGGER.finest("Snapshot reactivation failed! " +
×
245
                                    "Closing already reactivated context: " + alreadyReactivated + ".");
246
                        }
247
                        alreadyReactivated.close();
1✔
NEW
248
                    } catch (RuntimeException rte) {
×
NEW
249
                        reactivationException.addSuppressed(rte);
×
250
                    }
1✔
251
                }
1✔
252
                contextManagers = null;
1✔
253
                throw reactivationException;
1✔
254
            }
255
        }
256

257
        @SuppressWarnings("unchecked") // As we got the values from the managers themselves, they must also accept them!
258
        private Context reactivate(ContextManager contextManager, Object snapshotValue) {
259
            long start = System.nanoTime();
1✔
260
            Context reactivated = contextManager.initializeNewContext(snapshotValue);
1✔
261
            if (LOGGER.isLoggable(Level.FINEST)) {
1✔
NEW
262
                LOGGER.finest("Context reactivated from snapshot by " + contextManager + ": " + reactivated + ".");
×
263
            }
264
            Timers.timed(System.nanoTime() - start, contextManager.getClass(), "initializeNewContext");
1✔
265
            return reactivated;
1✔
266
        }
267

268
        @Override
269
        public String toString() {
270
            return "ContextSnapshot{size=" + managers.length + '}';
1✔
271
        }
272
    }
273

274
    /**
275
     * Implementation of the reactivated 'container' context that closes all reactivated contexts
276
     * when it is closed itself.<br>
277
     * This context contains no meaningful value in itself and purely exists to close the reactivated contexts.
278
     */
279
    private static final class ReactivationImpl implements Reactivation {
280
        private final List<Context<?>> reactivated;
281

282
        private ReactivationImpl(List<Context<?>> reactivated) {
1✔
283
            this.reactivated = reactivated;
1✔
284
        }
1✔
285

286
        public void close() {
287
            RuntimeException closeException = null;
1✔
288
            // close in reverse order of reactivation
289
            for (int i = this.reactivated.size() - 1; i >= 0; i--) {
1✔
290
                Context<?> reactivated = this.reactivated.get(i);
1✔
291
                if (reactivated != null) try {
1✔
292
                    reactivated.close();
1✔
293
                } catch (RuntimeException rte) {
1✔
294
                    if (closeException == null) closeException = rte;
1✔
NEW
295
                    else closeException.addSuppressed(rte);
×
296
                }
1✔
297
            }
298
            if (closeException != null) throw closeException;
1✔
299
        }
1✔
300

301
        @Override
302
        public String toString() {
303
            return "ReactivatedContext{size=" + reactivated.size() + '}';
1✔
304
        }
305
    }
306

307
    /**
308
     * Exception that we don't actually throw, but it helps track the issue if we log it including the stacktrace.
309
     */
310
    private static class NoContextManagersFound extends RuntimeException {
311
        private NoContextManagersFound() {
312
            super("Context snapshot was created but no ContextManagers were found!"
1✔
313
                    + " Thread=" + Thread.currentThread()
1✔
314
                    + ", ContextClassLoader=" + Thread.currentThread().getContextClassLoader());
1✔
315
        }
1✔
316
    }
317
}
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

© 2026 Coveralls, Inc