Skip to content

Commit 2260ab5

Browse files
katcharovstIncMale
andauthored
Add async API (2) (#1258)
* Add async functions * Update async functions * Add completeExceptionally * Fix NP warning * Move and update docs, fix cast * Apply suggestions from code review Co-authored-by: Valentin Kovalenko <[email protected]> * Address PR comments --------- Co-authored-by: Valentin Kovalenko <[email protected]>
1 parent a63e825 commit 2260ab5

File tree

11 files changed

+1654
-15
lines changed

11 files changed

+1654
-15
lines changed

driver-core/src/main/com/mongodb/assertions/Assertions.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public static <T> Iterable<T> notNullElements(final String name, final Iterable<
9292
public static <T> T notNull(final String name, final T value, final SingleResultCallback<?> callback) {
9393
if (value == null) {
9494
IllegalArgumentException exception = new IllegalArgumentException(name + " can not be null");
95-
callback.onResult(null, exception);
95+
callback.completeExceptionally(exception);
9696
throw exception;
9797
}
9898
return value;
@@ -122,7 +122,7 @@ public static void isTrue(final String name, final boolean condition) {
122122
public static void isTrue(final String name, final boolean condition, final SingleResultCallback<?> callback) {
123123
if (!condition) {
124124
IllegalStateException exception = new IllegalStateException("state should be: " + name);
125-
callback.onResult(null, exception);
125+
callback.completeExceptionally(exception);
126126
throw exception;
127127
}
128128
}

driver-core/src/main/com/mongodb/connection/AsyncCompletionHandler.java

+14
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.mongodb.connection;
1818

19+
import com.mongodb.internal.async.SingleResultCallback;
1920
import com.mongodb.lang.Nullable;
2021

2122
/**
@@ -38,4 +39,17 @@ public interface AsyncCompletionHandler<T> {
3839
* @param t the exception that describes the failure
3940
*/
4041
void failed(Throwable t);
42+
43+
/**
44+
* @return this handler as a callback
45+
*/
46+
default SingleResultCallback<T> asCallback() {
47+
return (r, t) -> {
48+
if (t != null) {
49+
failed(t);
50+
} else {
51+
completed(r);
52+
}
53+
};
54+
}
4155
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
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+
17+
package com.mongodb.internal.async;
18+
19+
/**
20+
* See {@link AsyncRunnable}.
21+
* <p>
22+
* This class is not part of the public API and may be removed or changed at any time
23+
*/
24+
@FunctionalInterface
25+
public interface AsyncConsumer<T> extends AsyncFunction<T, Void> {
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
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+
17+
package com.mongodb.internal.async;
18+
19+
import com.mongodb.lang.Nullable;
20+
21+
/**
22+
* See {@link AsyncRunnable}
23+
* <p>
24+
* This class is not part of the public API and may be removed or changed at any time
25+
*/
26+
@FunctionalInterface
27+
public interface AsyncFunction<T, R> {
28+
/**
29+
* This should not be called externally, but should be implemented as a
30+
* lambda. To "finish" an async chain, use one of the "finish" methods.
31+
*
32+
* @param value A {@code @}{@link Nullable} argument of the asynchronous function.
33+
* @param callback the callback
34+
*/
35+
void unsafeFinish(T value, SingleResultCallback<R> callback);
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
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+
17+
package com.mongodb.internal.async;
18+
19+
import com.mongodb.internal.async.function.RetryState;
20+
import com.mongodb.internal.async.function.RetryingAsyncCallbackSupplier;
21+
22+
import java.util.function.Predicate;
23+
import java.util.function.Supplier;
24+
25+
/**
26+
* <p>See the test code (AsyncFunctionsTest) for API usage.
27+
*
28+
* <p>This API is used to write "Async" methods. These must exhibit the
29+
* same behaviour as their sync counterparts, except asynchronously,
30+
* and will make use of a {@link SingleResultCallback} parameter.
31+
*
32+
* <p>This API makes it easy to compare and verify async code against
33+
* corresponding sync code, since the "shape" and ordering of the
34+
* async code matches that of the sync code. For example, given the
35+
* following "sync" method:
36+
*
37+
* <pre>
38+
* public T myMethod()
39+
* method1();
40+
* method2();
41+
* }</pre>
42+
*
43+
* <p>The async counterpart would be:
44+
*
45+
* <pre>
46+
* public void myMethodAsync(SingleResultCallback&lt;T> callback)
47+
* beginAsync().thenRun(c -> {
48+
* method1Async(c);
49+
* }).thenRun(c -> {
50+
* method2Async(c);
51+
* }).finish(callback);
52+
* }
53+
* </pre>
54+
*
55+
* <p>The usage of this API is defined in its tests (AsyncFunctionsTest).
56+
* Each test specifies the Async API code that must be used to formally
57+
* replace a particular pattern of sync code. These tests, in a sense,
58+
* define formal rules of replacement.
59+
*
60+
* <p>Requirements and conventions:
61+
*
62+
* <p>Each async method SHOULD start with {@link #beginAsync()}, which begins
63+
* a chain of lambdas. Each lambda provides a callback "c" that MUST be passed
64+
* or completed at the lambda's end of execution. The async method's "callback"
65+
* parameter MUST be passed to {@link #finish(SingleResultCallback)}, and MUST
66+
* NOT be used otherwise.
67+
*
68+
* <p>Consider refactoring corresponding sync code to reduce nesting or to
69+
* otherwise improve clarity, since minor issues will often be amplified in
70+
* the async code.
71+
*
72+
* <p>Each async lambda MUST invoke its async method with "c", and MUST return
73+
* immediately after invoking that method. It MUST NOT, for example, have
74+
* a catch or finally (including close on try-with-resources) after the
75+
* invocation of the async method.
76+
*
77+
* <p>In cases where the async method has "mixed" returns (some of which are
78+
* plain sync, some async), the "c" callback MUST be completed on the
79+
* plain sync path, using {@link SingleResultCallback#complete(Object)} or
80+
* {@link SingleResultCallback#complete(SingleResultCallback)}, followed by a
81+
* return or end of method.
82+
*
83+
* <p>Chains starting with {@link #beginAsync()} correspond roughly to code
84+
* blocks. This includes method bodies, blocks used in if/try/catch/while/etc.
85+
* statements, and places where anonymous code blocks might be used. For
86+
* clarity, such nested/indented chains might be omitted (where possible,
87+
* as demonstrated in tests).
88+
*
89+
* <p>Plain sync code MAY throw exceptions, and SHOULD NOT attempt to handle
90+
* them asynchronously. The exceptions will be caught and handled by the API.
91+
*
92+
* <p>All code, including "plain" code (parameter checks) SHOULD be placed
93+
* within the API's async lambdas. This ensures that exceptions are handled,
94+
* and facilitates comparison/review. This excludes code that must be
95+
* "shared", such as lambda and variable declarations.
96+
*
97+
* <p>For consistency, and ease of comparison/review, async chains SHOULD be
98+
* formatted as in the tests; that is, with line-breaks at the curly-braces of
99+
* lambda bodies, with no linebreak before the "." of any Async API method.
100+
*
101+
* <p>Code review checklist, for common mistakes:
102+
*
103+
* <ol>
104+
* <li>Is everything (that can be) inside the async lambdas?</li>
105+
* <li>Is "callback" supplied to "finish"?</li>
106+
* <li>In each block and nested block, is that same block's "c" always
107+
* passed/completed at the end of execution?</li>
108+
* <li>Is every c.complete followed by a return, to end execution?</li>
109+
* <li>Have all sync method calls been converted to async, where needed?</li>
110+
* </ol>
111+
*
112+
* <p>This class is not part of the public API and may be removed or changed
113+
* at any time
114+
*/
115+
@FunctionalInterface
116+
public interface AsyncRunnable extends AsyncSupplier<Void>, AsyncConsumer<Void> {
117+
118+
static AsyncRunnable beginAsync() {
119+
return (c) -> c.complete(c);
120+
}
121+
122+
/**
123+
* Must be invoked at end of async chain
124+
* @param runnable the sync code to invoke (under non-exceptional flow)
125+
* prior to the callback
126+
* @param callback the callback provided by the method the chain is used in
127+
*/
128+
default void thenRunAndFinish(final Runnable runnable, final SingleResultCallback<Void> callback) {
129+
this.finish((r, e) -> {
130+
if (e != null) {
131+
callback.completeExceptionally(e);
132+
return;
133+
}
134+
try {
135+
runnable.run();
136+
} catch (Throwable t) {
137+
callback.completeExceptionally(t);
138+
return;
139+
}
140+
callback.complete(callback);
141+
});
142+
}
143+
144+
/**
145+
* See {@link #thenRunAndFinish(Runnable, SingleResultCallback)}, but the runnable
146+
* will always be executed, including on the exceptional path.
147+
* @param runnable the runnable
148+
* @param callback the callback
149+
*/
150+
default void thenAlwaysRunAndFinish(final Runnable runnable, final SingleResultCallback<Void> callback) {
151+
this.finish((r, e) -> {
152+
try {
153+
runnable.run();
154+
} catch (Throwable t) {
155+
if (e != null) {
156+
t.addSuppressed(e);
157+
}
158+
callback.completeExceptionally(t);
159+
return;
160+
}
161+
callback.onResult(r, e);
162+
});
163+
}
164+
165+
/**
166+
* @param runnable The async runnable to run after this runnable
167+
* @return the composition of this runnable and the runnable, a runnable
168+
*/
169+
default AsyncRunnable thenRun(final AsyncRunnable runnable) {
170+
return (c) -> {
171+
this.unsafeFinish((r, e) -> {
172+
if (e == null) {
173+
runnable.unsafeFinish(c);
174+
} else {
175+
c.completeExceptionally(e);
176+
}
177+
});
178+
};
179+
}
180+
181+
/**
182+
* @param condition the condition to check
183+
* @param runnable The async runnable to run after this runnable,
184+
* if and only if the condition is met
185+
* @return the composition of this runnable and the runnable, a runnable
186+
*/
187+
default AsyncRunnable thenRunIf(final Supplier<Boolean> condition, final AsyncRunnable runnable) {
188+
return (callback) -> {
189+
this.unsafeFinish((r, e) -> {
190+
if (e != null) {
191+
callback.completeExceptionally(e);
192+
return;
193+
}
194+
boolean matched;
195+
try {
196+
matched = condition.get();
197+
} catch (Throwable t) {
198+
callback.completeExceptionally(t);
199+
return;
200+
}
201+
if (matched) {
202+
runnable.unsafeFinish(callback);
203+
} else {
204+
callback.complete(callback);
205+
}
206+
});
207+
};
208+
}
209+
210+
/**
211+
* @param supplier The supplier to supply using after this runnable
212+
* @return the composition of this runnable and the supplier, a supplier
213+
* @param <R> The return type of the resulting supplier
214+
*/
215+
default <R> AsyncSupplier<R> thenSupply(final AsyncSupplier<R> supplier) {
216+
return (c) -> {
217+
this.unsafeFinish((r, e) -> {
218+
if (e == null) {
219+
supplier.unsafeFinish(c);
220+
} else {
221+
c.completeExceptionally(e);
222+
}
223+
});
224+
};
225+
}
226+
227+
/**
228+
* @param runnable the runnable to loop
229+
* @param shouldRetry condition under which to retry
230+
* @return the composition of this, and the looping branch
231+
* @see RetryingAsyncCallbackSupplier
232+
*/
233+
default AsyncRunnable thenRunRetryingWhile(
234+
final AsyncRunnable runnable, final Predicate<Throwable> shouldRetry) {
235+
return thenRun(callback -> {
236+
new RetryingAsyncCallbackSupplier<Void>(
237+
new RetryState(),
238+
(rs, lastAttemptFailure) -> shouldRetry.test(lastAttemptFailure),
239+
// `finish` is required here instead of `unsafeFinish`
240+
// because only `finish` meets the contract of
241+
// `AsyncCallbackSupplier.get`, which we implement here
242+
cb -> runnable.finish(cb)
243+
).get(callback);
244+
});
245+
}
246+
}

0 commit comments

Comments
 (0)