|
4 | 4 | import lombok.NoArgsConstructor; |
5 | 5 | import lombok.NonNull; |
6 | 6 |
|
| 7 | +import java.lang.Error; |
7 | 8 | import java.util.ArrayList; |
8 | 9 | import java.util.List; |
| 10 | +import java.util.concurrent.CompletableFuture; |
| 11 | +import java.util.function.Consumer; |
9 | 12 |
|
10 | 13 | /** |
11 | | - * A scoped defer stack, similar to the <code>defer</code> statement in Go. |
| 14 | + * A scoped stack of deferred cleanup actions, similar to Go's {@code defer}. |
12 | 15 | * <p> |
13 | | - * <code>Defer</code> allows registering cleanup actions that are executed |
14 | | - * automatically when the scope is exited (via {@link #close()}), typically by |
15 | | - * using a {@code try}-with-resources block. |
| 16 | + * {@code Defer} allows you to register cleanup callbacks during execution, |
| 17 | + * which are automatically invoked when the scope ends. Deferred actions |
| 18 | + * run in <strong>LIFO</strong> (last-in-first-out) order. |
16 | 19 | * |
17 | | - * <p>Deferred actions are executed in <strong>reverse order</strong> of registration, |
18 | | - * ensuring proper cleanup behavior (LIFO). |
19 | | - * |
20 | | - * <p>Example: |
| 20 | + * <p>Typical usage: |
21 | 21 | * <pre>{@code |
22 | 22 | * try (Defer d = Defer.create()) { |
23 | | - * d.defer(() -> System.out.println("cleanup 1")); |
24 | | - * d.defer(() -> System.out.println("cleanup 2")); |
25 | | - * System.out.println("doing work"); |
| 23 | + * d.defer(() -> System.out.println("cleanup A")); |
| 24 | + * d.defer(() -> System.out.println("cleanup B")); |
| 25 | + * System.out.println("work"); |
26 | 26 | * } |
27 | 27 | * |
28 | 28 | * // Output: |
29 | | - * // doing work |
30 | | - * // cleanup 2 |
31 | | - * // cleanup 1 |
| 29 | + * // work |
| 30 | + * // cleanup B |
| 31 | + * // cleanup A |
32 | 32 | * }</pre> |
33 | 33 | * |
34 | | - * <p><strong>Error Handling:</strong><br> |
35 | | - * If deferred actions throw exceptions, they are caught and suppressed silently. |
36 | | - * This ensures that all cleanup actions are attempted. |
| 34 | + * <p><strong>Exception Handling:</strong><br> |
| 35 | + * If multiple deferred actions throw exceptions: |
| 36 | + * <ul> |
| 37 | + * <li>The first thrown exception is rethrown</li> |
| 38 | + * <li>Additional exceptions are attached using {@link Throwable#addSuppressed(Throwable)}</li> |
| 39 | + * </ul> |
| 40 | + * |
| 41 | + * <p>This makes {@code Defer} safe for resource cleanup, multi-step teardown, |
| 42 | + * and transactional compensation. |
37 | 43 | */ |
38 | 44 | @NoArgsConstructor (access = AccessLevel.PRIVATE) |
39 | 45 | public final class Defer implements AutoCloseable { |
| 46 | + |
| 47 | + @FunctionalInterface |
| 48 | + private interface Cleanup { |
| 49 | + void run() throws Exception; |
| 50 | + } |
| 51 | + |
40 | 52 | /** |
41 | | - * The stack of deferred actions. |
42 | | - * Executed in reverse order when {@link #close()} is called. |
| 53 | + * Internal LIFO stack of cleanup actions. |
43 | 54 | */ |
44 | | - private final List<Runnable> stack = new ArrayList<>(); |
| 55 | + private final List<Cleanup> stack = new ArrayList<>(); |
| 56 | + |
| 57 | + /** |
| 58 | + * Creates a new, initially empty {@code Defer} stack. |
| 59 | + * |
| 60 | + * @return a new {@code Defer} instance |
| 61 | + */ |
| 62 | + public static Defer create() { |
| 63 | + return new Defer(); |
| 64 | + } |
45 | 65 |
|
46 | 66 | /** |
47 | | - * Creates a new, empty defer stack. |
| 67 | + * Executes a scoped block with automatic cleanup. |
48 | 68 | * <p> |
49 | | - * Intended to be used with {@code try}-with-resources: |
| 69 | + * Equivalent to: |
50 | 70 | * <pre>{@code |
51 | 71 | * try (Defer d = Defer.create()) { |
52 | | - * ... |
| 72 | + * body.accept(d); |
53 | 73 | * } |
54 | 74 | * }</pre> |
55 | 75 | * |
56 | | - * @return a new {@link Defer} instance |
| 76 | + * @param body the scoped operation to execute |
| 77 | + * @throws Exception if either the body or cleanup throws |
57 | 78 | */ |
58 | | - public static Defer create() { |
59 | | - return new Defer(); |
| 79 | + public static void scope(@NonNull Consumer<Defer> body) throws Exception { |
| 80 | + try (Defer d = create()) { |
| 81 | + body.accept(d); |
| 82 | + } |
60 | 83 | } |
61 | 84 |
|
62 | 85 | /** |
63 | | - * Registers a new action to be executed when this {@code Defer} is closed. |
64 | | - * The action will be executed after all previously registered actions. |
| 86 | + * Registers an arbitrary action to be executed when this {@code Defer} is closed. |
| 87 | + * <p> |
| 88 | + * The action is executed after all previously registered actions. |
65 | 89 | * |
66 | 90 | * @param r the action to defer |
| 91 | + * @return this {@code Defer}, enabling chained calls |
| 92 | + */ |
| 93 | + public Defer defer(@NonNull Runnable r) { |
| 94 | + synchronized (stack) { |
| 95 | + stack.add(r::run); |
| 96 | + } |
| 97 | + return this; |
| 98 | + } |
| 99 | + |
| 100 | + /** |
| 101 | + * Registers a closeable resource which will be closed when this {@code Defer} is closed. |
| 102 | + * |
| 103 | + * @param c the resource to close |
| 104 | + * @return this {@code Defer}, enabling chained calls |
| 105 | + */ |
| 106 | + public Defer defer(@NonNull AutoCloseable c) { |
| 107 | + synchronized (stack) { |
| 108 | + stack.add(c::close); |
| 109 | + } |
| 110 | + return this; |
| 111 | + } |
| 112 | + |
| 113 | + /** |
| 114 | + * Registers a {@link CompletableFuture} to be awaited on scope exit. |
| 115 | + * Its result value is ignored; errors propagate into cleanup. |
| 116 | + * |
| 117 | + * @param future the future to complete before leaving the scope |
| 118 | + * @return this {@code Defer}, enabling chained calls |
| 119 | + */ |
| 120 | + public Defer defer(@NonNull CompletableFuture<?> future) { |
| 121 | + synchronized (stack) { |
| 122 | + stack.add(future::get); |
| 123 | + } |
| 124 | + return this; |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * Returns the number of deferred cleanup actions currently stored. |
| 129 | + * |
| 130 | + * @return number of registered deferred actions |
67 | 131 | */ |
68 | | - public void defer(@NonNull Runnable r) { |
69 | | - stack.add(r); |
| 132 | + public int size() { |
| 133 | + synchronized (stack) { |
| 134 | + return stack.size(); |
| 135 | + } |
70 | 136 | } |
71 | 137 |
|
72 | 138 | /** |
73 | | - * Executes all deferred actions in reverse order. |
| 139 | + * Executes deferred actions in <strong>reverse</strong> order. |
74 | 140 | * <p> |
75 | | - * Any exceptions thrown by deferred actions are caught and ignored, allowing |
76 | | - * all actions to run regardless of failures. |
| 141 | + * Errors follow a predictable structured-error model: |
| 142 | + * <ul> |
| 143 | + * <li>If no errors occur, nothing is thrown.</li> |
| 144 | + * <li>If one error occurs, it is thrown.</li> |
| 145 | + * <li>If multiple errors occur, the first is thrown and the rest are suppressed.</li> |
| 146 | + * </ul> |
| 147 | + * |
| 148 | + * @throws Exception the primary exception thrown during cleanup |
77 | 149 | */ |
78 | 150 | @Override |
79 | | - public void close() { |
80 | | - for (int i = stack.size() - 1; i >= 0; i--) { |
81 | | - try { |
82 | | - stack.get(i).run(); |
83 | | - } catch (Exception _) {} |
| 151 | + public void close() throws Exception { |
| 152 | + Throwable primary = null; |
| 153 | + |
| 154 | + synchronized (stack) { |
| 155 | + for (int i = stack.size() - 1; i >= 0; i--) { |
| 156 | + try { |
| 157 | + stack.get(i).run(); |
| 158 | + } catch (Throwable t) { |
| 159 | + if (primary == null) { |
| 160 | + primary = t; |
| 161 | + continue; |
| 162 | + } |
| 163 | + primary.addSuppressed(t); |
| 164 | + } |
| 165 | + } |
84 | 166 | } |
| 167 | + if (primary instanceof Exception e) throw e; |
| 168 | + if (primary instanceof Error err) throw err; |
| 169 | + if (primary == null) return; |
| 170 | + throw new RuntimeException(primary); |
| 171 | + } |
| 172 | + |
| 173 | + @Override |
| 174 | + public String toString() { |
| 175 | + return "Defer[size=" + size() + "]"; |
85 | 176 | } |
86 | 177 | } |
0 commit comments