-
Notifications
You must be signed in to change notification settings - Fork 856
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Async pagination #419
Conversation
|
||
@Override | ||
protected String appendToDescription() { | ||
return opModel.isPaginated() ? paginationDocs.getDocsForAsyncOperation() : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return null will cause a Null Dereference
risk on fortify, we can probably return Optional here
f7c140d
to
6beb521
Compare
6beb521
to
4b7f0cd
Compare
+ "see {@link $T#$L($T)}. If there are errors in your " | ||
+ "request, you will see the failures only after you start streaming the data.</p>", | ||
getPublisherType(), SUBSCRIBE_METHOD_NAME, getSubscriberType()) | ||
.add(getAsyncCodeSnippets()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra space here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
/** | ||
* @return A Poet {@link ClassName} for the reactive streams {@link Publisher}. | ||
*/ | ||
private ClassName getPublisherType() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need this and the one below in separate methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are used 2-3 places in the class. I thought it would be better to have them in separate method rather than calling the ClassName.get() method.
return new PoetGeneratorTask(paginatorsClassDir, model.getFileHeader(), classSpec); | ||
} | ||
|
||
private Stream<GeneratorTask> createASyncTasks() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ASync -> Async
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
*/ | ||
@ReviewBeforeRelease("Naming of response shape for paginated APIs") | ||
public ClassName getResponseClassForPaginatedSyncOperation(String operationName) { | ||
return ClassName.get(model.getMetadata().getFullPaginatorsPackageName(), operationName + "Paginator"); | ||
return ClassName.get(model.getMetadata().getFullPaginatorsPackageName(), operationName + "Iterable"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the change in name here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method ends with "Paginator", so didn't want same name for response type. For example,
ListTablesIterable listTablesPaginator(ListTablesRequest);
over
ListTablesPaginator listTablesPaginator(ListTablesRequest);
This also conveys the return type is a Iterable. It also matches the convention for async method.
// Async method declaration
ListTablesPublisher listTablesPaginator(ListTablesRequest);
return paginatedTraditionalMethodBody(builder, opModel).build(); | ||
} | ||
|
||
protected MethodSpec.Builder paginatedTraditionalMethodBody(MethodSpec.Builder builder, OperationModel operationModel) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this a separate method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is overriden in AsyncClientClass
* 1) Using the forEach helper method. This uses @ | ||
* {@link software.amazon.awssdk.core.pagination.async.SequentialSubscriber} internally | ||
* | ||
* <pre> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this all look good in javadoc?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting is good. But classes are displayed with FQCN. So it looks little verbose.
subscriber.onSubscribe(new ItemsSubscription(subscriber)); | ||
} | ||
|
||
private class ItemsSubscription implements org.reactivestreams.Subscription { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we using the full fqcn import?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
@@ -247,6 +247,11 @@ | |||
<artifactId>netty-reactive-streams-http</artifactId> | |||
<version>2.0.0</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>io.reactivex.rxjava2</groupId> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the size of this dependency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Size is 2.1MB. I added it to show that customer can use 3rd party implementations for additional helper methods. I can remove it if we decide so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a test dependency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I have marked the scope as test in services pom. Do we need it here too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No just where it's used is fine. Just making sure it's a test dependency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still looking at it, will review more later.
|
||
@Override | ||
protected String appendToDescription() { | ||
return opModel.isPaginated() ? paginationDocs.getDocsForAsyncOperation() : ""; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to check isPaginated? Would this be invoked if the operation isn't paginated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, removed the check.
.add("<p>When the operation is called, an instance of this class is returned. At this point, " | ||
+ "no service calls are made yet and so there is no guarantee that the request is valid. " | ||
+ "The subscribe method should be called as a request to stream data. For more info, " | ||
+ "see {@link $T#$L($T)}. If there are errors in your " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a question for both sync and async pagination. How do we handle retryable/transient errors while paginating? Is the iterable/publisher considered terminated at that point and the customer has to make a new request? Is there a way they can get the request object to reconnect (i.e. without getting the last response tokens manually)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed offline, added the resume() method in the Response.
syncResponsePageType()) | ||
.add("<p>When the operation is called, an instance of this class is returned. At this point, " | ||
+ "no service calls are made yet and so there is no guarantee that the request is valid. " | ||
+ "The subscribe method should be called as a request to stream data. For more info, " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add some documentation on what multiple subscribes means? I.E. each one is a separate iteration from the starting request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
|
||
return CodeBlock.builder() | ||
.add("\n\n<p>The following are few ways to use the response class:</p>") | ||
.add("1) Using the forEach helper method. This uses @{@link $T} internally", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do they need to care what it uses internally?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, removed.
.add(buildCode(CodeBlock.builder() | ||
.add(callOperationOnClient) | ||
.add(CodeBlock.builder() | ||
.addStatement("CompletableFuture<Void> future = publisher" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the future here be fulfilled with the last response? Would that be useful or confusing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where would you do it? Can you show me sample code
.collect(Collectors.toList()); | ||
} | ||
|
||
private Stream<GeneratorTask> createSyncTasks() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to reduce a little duplication here and create a method that takes in this::createSyncTask this::createAsyncTask respectively.
@@ -128,6 +129,14 @@ private MethodSpec closeMethod() { | |||
|
|||
} | |||
|
|||
@Override | |||
protected MethodSpec.Builder paginatedTraditionalMethodBody(MethodSpec.Builder builder, OperationModel opModel) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does traditional mean in this context? Non-simple?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, a method that takes in a request and returns a response.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to use "traditional" here (vs nothing) and in other cases? Are there any cases beyond paginated methods and paginated simple methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. There is no need for the "traditional" part as its the only method body for paginated operations. Will remove.
* {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} | ||
* operation.</b> | ||
* </p> | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why no onError and onComplete in the custom Subscriber example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As the body of the methods are empty, didn't think it would be useful to add all of them and clutter the docs. Anyways when customers tries to add code like this, compiler will give errors about the missing methods
* </p> | ||
* | ||
* @param paginatedOperationWithResultKeyRequest | ||
* @return A Java Future containing the result of the PaginatedOperationWithResultKey operation returned by the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incorrect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed the return statement.
Needs some more changes to fix the next line (prefix for async throws statement)
* @see <a href="http://docs.aws.amazon.com/goto/WebAPI/json-service-2010-05-08/PaginatedOperationWithResultKey" | ||
* target="_top">AWS API Documentation</a> | ||
*/ | ||
default PaginatedOperationWithResultKeyPublisher paginatedOperationWithResultKeyPaginator( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a test that exercises the no arg simple method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will add
|
||
outstandingRequests.addAndGet(n); | ||
|
||
synchronized (this) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use compareAndSet here and lose the lock?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
|
||
private void handleRequestsRecursively() { | ||
if (currentPage != null && !nextPageFetcher.hasNextPage(currentPage) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This expression is a bit hard to read. Some methods may help?
if(!(hasNextPage() || hasMoreItems())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
|
||
private void fetchNextPage() { | ||
CompletableFuture<ResponseT> future = nextPageFetcher.nextPage(currentPage); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inline future
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
sendNextElement(); | ||
} | ||
if (error != null) { | ||
subscriber.onError(error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there something that tells the publisher to stop publishing items?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a isTerminated flag
|
||
@Override | ||
public void cancel() { | ||
outstandingRequests.set(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would future calls to request(n) re-activate the subscription? Should we guard against that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a isTerminated flag
* @param <ResponseT> The type of a single response page | ||
* @param <ItemT> The type of paginated member in a response page | ||
*/ | ||
public class PaginatedItemsPublisher<ResponseT, ItemT> implements SdkPublisher<ItemT> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a lot of duplicated code from the Response publisher. Is there a way to consolidate?
This code from Rx might be relevant for making this more abstract
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't use the above link. But abstracted out the common code.
|
||
outstandingRequests.getAndDecrement(); | ||
|
||
CompletableFuture<ResponseT> future = nextPageFetcher.nextPage(currentPage); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inline
return; | ||
} | ||
|
||
if (outstandingRequests.get() <= 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking there's a race condition between this and the code that initiates a new task.
Evaluation order
If(outstandingRequests.get() <= 0) evalues to true
customer requests more demand
outstandingRequests incremented
we check if task is still running, see that it is, and bail
we set isTaskRunning to false here and now we've dropped a request
I think we would need to acquire the same lock to prevent this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Modified the code a bit. Still looking for race conditions
de3b0e6
to
cdcba96
Compare
@@ -128,6 +129,14 @@ private MethodSpec closeMethod() { | |||
|
|||
} | |||
|
|||
@Override | |||
protected MethodSpec.Builder paginatedTraditionalMethodBody(MethodSpec.Builder builder, OperationModel opModel) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to use "traditional" here (vs nothing) and in other cases? Are there any cases beyond paginated methods and paginated simple methods?
return new PaginatedOperationWithResultKeyIterable(client, firstRequest.toBuilder() | ||
.nextToken(lastSuccessfulPage.nextToken()).build()); | ||
} | ||
return new PaginatedOperationWithResultKeyIterable(client, firstRequest) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the behavior here? To return an empty list if no additional pages are available?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If customer tries to resume from last page (for some reason), the iterator method would return empty iterator as there would be no more pages.
|
||
private final AsyncPageFetcher nextPageFetcher; | ||
|
||
private boolean isLastPage; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's this used for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In resume method, we return EmptySubscription (EmptyIterator for sync) on subscribe if there are no more pages. This works when customer subscribes to get pages. If you try to subscribe to get Items (PaginatedItemsPublisher class), there is currently no way to recognize if input is last page and return EmptySubscription. The two options I see are:
- Pass a value to PaginatedItemsPublisher to indicate its last page (current approach)
- Override all the items methods in resume method to use EmptySubscription (if there are no more pages). I felt this will bloat all the publisher classes and chose option 1.
The resume method in option 2 will look like:
public PaginatedOperationWithResultKeyPublisher resume(final PaginatedOperationWithResultKeyResponse lastSuccessfulPage) {
if (nextPageFetcher.hasNextPage(lastSuccessfulPage)) {
return new PaginatedOperationWithResultKeyPublisher(client, firstRequest.toBuilder()
.nextToken(lastSuccessfulPage.nextToken()).build());
}
return new PaginatedOperationWithResultKeyPublisher(client, firstRequest) {
@Override
public void subscribe(Subscriber<? super PaginatedOperationWithResultKeyResponse> subscriber) {
subscriber.onSubscribe(new EmptySubscription(subscriber));
}
@Override
public SdkPublisher<SimpleStruct> items() {
return new EmptyPublisher<>(); // A new publisher that creates EmptySubscription when subscribed to
}
//More overriden methods if there are multiple result keys
};
}
cdcba96
to
bc78fb0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll finish looking at this tonight.
throw new IllegalArgumentException("Non-positive request signals are illegal"); | ||
} | ||
|
||
subscriber.onComplete(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition here, if I queue up two requests then I can get two completes.
This could should fix it.
if(isTerminated.compareAndSet(false, true)) {
subscriber.onComplete();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
}.withLastPage(true); | ||
} | ||
|
||
PaginatedOperationWithoutResultKeyPublisher withLastPage(boolean isLastPage) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need a setter for this? Can't you just set it in the constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be set in constructor too. But having it in setter might hide it better constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually thinking again, having a private constructor is better than the setter. Will fix it.
bc78fb0
to
8558232
Compare
60e8177
to
813a7c2
Compare
813a7c2
to
3b12e24
Compare
Add support for Async Pagination
#185
Description
Generates extra methods in client and interface for paginated methods. These methods return a custom publisher that can be used to request a stream of data without the customer explicitly making service calls.
Added RxJava2 as test dependency to show a test case on using it. I am not fully satisfied with the async user experience using the pub-sub interfaces. Using third party reactive streams implementations will help in making experience better as there are lot of helper methods.
Edit1
Motivation and Context
#185
Testing
Added unit tests
See ListObjectsV2PaginatorsIntegrationTest.java for integration test and how customer uses
Types of changes
Checklist
mvn install
succeedsLicense