Skip to content

Commit c21603b

Browse files
feat: Added more utils and docs to Defer
1 parent 886a226 commit c21603b

1 file changed

Lines changed: 129 additions & 38 deletions

File tree

src/main/java/gentle/Defer.java

Lines changed: 129 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,83 +4,174 @@
44
import lombok.NoArgsConstructor;
55
import lombok.NonNull;
66

7+
import java.lang.Error;
78
import java.util.ArrayList;
89
import java.util.List;
10+
import java.util.concurrent.CompletableFuture;
11+
import java.util.function.Consumer;
912

1013
/**
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}.
1215
* <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.
1619
*
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:
2121
* <pre>{@code
2222
* 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");
2626
* }
2727
*
2828
* // Output:
29-
* // doing work
30-
* // cleanup 2
31-
* // cleanup 1
29+
* // work
30+
* // cleanup B
31+
* // cleanup A
3232
* }</pre>
3333
*
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.
3743
*/
3844
@NoArgsConstructor (access = AccessLevel.PRIVATE)
3945
public final class Defer implements AutoCloseable {
46+
47+
@FunctionalInterface
48+
private interface Cleanup {
49+
void run() throws Exception;
50+
}
51+
4052
/**
41-
* The stack of deferred actions.
42-
* Executed in reverse order when {@link #close()} is called.
53+
* Internal LIFO stack of cleanup actions.
4354
*/
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+
}
4565

4666
/**
47-
* Creates a new, empty defer stack.
67+
* Executes a scoped block with automatic cleanup.
4868
* <p>
49-
* Intended to be used with {@code try}-with-resources:
69+
* Equivalent to:
5070
* <pre>{@code
5171
* try (Defer d = Defer.create()) {
52-
* ...
72+
* body.accept(d);
5373
* }
5474
* }</pre>
5575
*
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
5778
*/
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+
}
6083
}
6184

6285
/**
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.
6589
*
6690
* @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
67131
*/
68-
public void defer(@NonNull Runnable r) {
69-
stack.add(r);
132+
public int size() {
133+
synchronized (stack) {
134+
return stack.size();
135+
}
70136
}
71137

72138
/**
73-
* Executes all deferred actions in reverse order.
139+
* Executes deferred actions in <strong>reverse</strong> order.
74140
* <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
77149
*/
78150
@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+
}
84166
}
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() + "]";
85176
}
86177
}

0 commit comments

Comments
 (0)