Skip to content

Sync pagination implementation #207

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

Merged
merged 1 commit into from
Nov 14, 2017
Merged

Sync pagination implementation #207

merged 1 commit into from
Nov 14, 2017

Conversation

varunnvs92
Copy link
Contributor

@varunnvs92 varunnvs92 commented Oct 12, 2017

Description

Implementation for Sync automatic de-pagination. Supports lists, nested shapes in paginators.json file
Tracking Issue: #26

For all paginated operations, there will be two APIs - one with automatic depagination and one that returns a single page. Example: For listTables operation in DynamoDB, the following APIs are present in the client:

  1. ListTablesResponse listTables(ListTablesRequest listTablesRequest) - Returns only a single page. similar to v1
  2. ListTablesPaginator listTablesIterable(ListTablesRequest listTablesRequest)
    This is the auto paginated API in which SDK will make service calls for the customer.

As per recent api review discussions and inconsistencies in paginator files, going with this option is safe in terms of backwards compatibility. Coming to discoverability, these APIs are highly discoverable compared to high level utilities as they are in same client.

Recent changes

Added more unit tests
Added Javadocs for generated classes

Testing

mvn clean install
Added an integ test class

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Checklist

  • I have read the CONTRIBUTING document
  • Local run of mvn install succeeds
  • My code follows the code style of this project
  • My change requires a change to the Javadoc documentation
  • I have updated the Javadoc documentation accordingly
  • I have read the README document
  • I have added tests to cover my changes
  • All new and existing tests passed
  • A short description of the change has been added to the CHANGELOG

License

  • I confirm that this pull request can be released under the Apache 2 license

Copy link
Contributor

@shorea shorea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No major concerns so far. It would be nice to see the generated code to get a sense for what that looks like.

info("Emitting paginator classes");
List<GeneratorTask> tasks = new ArrayList<>();

model.getPaginators().entrySet().stream()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just collect into a list here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. changed

@@ -70,14 +75,16 @@ public IntermediateModel(
CustomizationConfig customizationConfig,
ServiceExamples examples,
Map<String, WaiterDefinitionModel> waiters,
Map<String, AuthorizerModel> customAuthorizers) {
Map<String, AuthorizerModel> customAuthorizers,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Builder might be nice here.

@@ -169,6 +180,10 @@ public boolean getHasWaiters() {
return waiters.size() > 0;
}

public boolean getHasPaginators() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This weird naming seems like something we would do for FTL. Do we need it for the poet approach?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waiters has the same naming. I thought it was used for serialization/deserialization. We might not need it. I can change it to hasPaginators()

* @return the name of the pagination enabled operation
*
*/
public static String getSyncMethodName(String methodName) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this make more sense in NamingStrategy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DefaultNamingStrategy constructor is coupled to ServiceModel and CustomizationConfig. But the method doesn't require any of them. The classes where I use this method don't have enough information to create a NamingStrategy object.

*/
public interface Paginated<PageT, ItemT> extends SdkIterable<PageT> {

PageT firstPage();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going with generating both request/response and paginated methods I question the usefulness of this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Customers can use this as return type instead of operation specific class.
For example,
Paginated<ListTablesResponse, String> response = dbClient.listTables();
Instead of
ListTablesPaginator response = dbClient.listTables();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I meant the firstPage specifically. That was added mainly when we were thinking of only exposing the paginator but if that's no longer true I don't think it holds it's weight.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are eagerly loading the responses. So if a customer want a single page and uses next() method, SDK makes two service calls. This method might help in that case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they just want a single page they can use the method that returns a single page. The only reason this was added was because we weren't planning on exposing the low level API method. Also I'm a little unsure about making service calls the customer didn't ask for, can we make it lazy loaded? You may still have to preemptively fetch the next page if hasNext is called (and you are at the end of a page boundary).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may still have to preemptively fetch the next page if hasNext is called (and you are at the end of a page boundary).

We don't want to make service calls in hasNext(). It can be called multiple times before next() method is called. I didn't get the statement about end of page boundary.


public class PaginatedItemsIterable<ResponseT, ItemT> implements SdkIterable<ItemT> {

private final Iterator<ResponseT> responseIterator;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be iterables too? Otherwise it seems you can only iterate it once.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everytime this object is constructed, a new object of iterator is passed. So if customer tries to use the API to iterate through response/paginated member multiple items, it works fine. I have added a sample paginator class in the description.

Copy link
Contributor

@shorea shorea Oct 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I'll have to see how this is used in the generated code but I assume it goes something like this.

// I assume this creates the PaginatedItemsIterable
SdkIterable<Reservation> reservations = ec2.describeInstances().allItems();  

// Iterate once
for(Reservation r : reserverations) {...}

// What happens when I iterate again?
for(Reservation r : reserverations) {...}

edit - Okay I see the generated code, yeah I think that will be a problem then right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. As discussed offline, will expose a PaginatedResponsesIterator instead of the PaginatedResponsesIterable and pass the paginator ref to this class

return new ItemIterator(responseIterator);
}

private class ItemIterator implements Iterator<ItemT> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well make this static and pass in the getPaginatedItemIterator too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The itemsIterator in the class is separate for each instance. I am not able to use the generics (ItemT) if I make the class static.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, can you just reference the outer field rather than pass in responseIterator? Just feels weird to pass one in but not the other.

throw new NoSuchElementException();
}

if (itemsIterator == null || !itemsIterator.hasNext()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

itemsIterator can never be null here right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a null case. Added it as a safety incase I overlooked any edge case.


@Override
public boolean hasNext() {
return (itemsIterator != null && itemsIterator.hasNext()) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For items iteration it's possible for the responseIterator to have a next page but that next page have an empty collection of items. We should make sure we are handling that case correctly in accordance with the Iterator interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for a page to have empty items? Then what is the use of that page? At this point in code, we will throw an exception as there is no next element. Its tricky to handle this case as we don't know if the page with empty items is the last page or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's possible, at least for Dynamo and I suspect for other services too. I think it's just an implementation limitation, the service may not know what the last item is until it goes to fetch it so if you end a page on the last item it thinks there might be more when in reality there's not. The only way to check for sure is to make another call and get an emtpy response with a empty paging token.

http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html

The primary key of the item where the operation stopped, inclusive of the previous result set. Use this value to start a new operation, excluding this value in the new request.
If LastEvaluatedKey is empty, then the "last page" of results has been processed and there is no more data to be retrieved.
If LastEvaluatedKey is not empty, it does not necessarily mean that there is more data in the result set. The only way to know when you have reached the end of the result set is when LastEvaluatedKey is empty.

private final Function<ResponseT, ResponseT> getNextResponse;

public PaginatedResponsesIterable(ResponseT firstResponse,
Function<ResponseT, ResponseT> getNextResponse) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of Function can we create an interface like NextResponseSupplier and annotate with @FunctionalInterface instead? That way we can have a more descriptive method like nextResponse instead of apply

Ditto for PaginatedItemsIterable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am okay with this change. Will do it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an interface for NextResponseSupplier.
I am not sure if I want to do the same for getIterator in PaginatedItemsIterable class. The result_key can be a list and we need to generate multiple classes for each key in the list. This will bloat the response class for paginated APIs.

@varunnvs92 varunnvs92 force-pushed the varunkn/SyncPagination branch 3 times, most recently from 860a2bb to 7aefbeb Compare October 18, 2017 17:47
@kiiadi
Copy link
Contributor

kiiadi commented Oct 18, 2017

re: naming, instead of listTablesAutoPaginated() can we call it something shorter? Maybe listTablesPaged() or listTablesPaginated() (I don't love these either but I think shorter is better)

@millems
Copy link
Contributor

millems commented Oct 18, 2017

To join the naming discussion, is it possible to come up with a name that follows our service API standards, since this will be on the service clients?

listTablesPaginated or listTablesPaged don't follow them, since the adjective would need to be before the noun. We don't have a way for separating the verb from the noun (and it would hurt discoverability), so could we possibly come up with a noun to use at the end instead of an adjective?

I'm probably just throwing fuel on the fire, though since I can't come up with an ending noun that doesn't require us to depluralize the resource's noun. listTablesPages seems weird (even if it's technically correct - it's a List<List<Tables>>).

@kiiadi
Copy link
Contributor

kiiadi commented Oct 18, 2017

listTablesIterable

@varunnvs92
Copy link
Contributor Author

varunnvs92 commented Oct 19, 2017

listTablesIterable

Sounds better as per english semantics. But It doesn't convey that the operation is paginated.

@varunnvs92 varunnvs92 force-pushed the varunkn/SyncPagination branch 2 times, most recently from 0bb679d to ab338c2 Compare October 19, 2017 06:53
@@ -43,6 +43,8 @@
MAPPER.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
// TODO: Un comment this once we know for sure, we capture everything in C2j model.
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// TODO: Risky to enable for all deserialiation?
MAPPER.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In paginators.json, the values for input_token, output_token and result_key can be a single value or a list of values.
Example:
"result_key": ["Folders", "Documents"]
"result_key": "Users"

I am representing these fields as List in PaginatorDefinition class. When deserializing the json file into PaginatorDefinition class, this property has to be set so mapper knows to accept a single value into a list.

@@ -52,6 +53,9 @@
@JsonIgnore
private final Map<String, WaiterDefinitionModel> waiters;

@JsonIgnore
private final Map<String, PaginatorDefinition> paginators;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep consistent naming? For waiters, its WaiterDefinitionModel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waiters are removed. Keep it as PaginatorDefinition.

@@ -147,14 +148,30 @@ private MethodSpec constructorWithAdvancedConfiguration() {
List<MethodSpec> methods = new ArrayList<>();
ClassName returnType = poetExtensions.getModelClass(opModel.getReturnType().getReturnType());

methods.add(SyncClientInterface.operationMethodSignature(model, opModel)
methods.add(SyncClientInterface.operationMethodSignature(model, opModel, opModel.getMethodName())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the change here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In operationMethodSignature method body, opModel.getMethodName() was being used operation name. For paginators enabled APIs, the name is different. I added the extra parameter in method signature so I can reuse operationMethodSignature() for both normal and pagination enabled APIs.

PaginatorUtils.getSyncMethodName(opModel.getMethodName()))
.addAnnotation(Override.class)
.returns(poetExtensions.getResponseClassForPaginatedSyncOperation(opModel.getOperationName()))
.addCode("\n")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the comment in only for adding new line, I don't see it in my latest revision. I might have added to have clear distinction if the body was complex.
Let me know if I misunderstood the comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the code is still there, I was looking at a different class. The new lines are to make the method body look clear in the generated class. I don't have a strong opinion on them.

.addAnnotation(Override.class)
.addCode(getCustomResponseHandler(opModel, returnType)
.orElseGet(() -> protocolSpec.responseHandler(opModel)))
.addCode(protocolSpec.errorResponseHandler(opModel))
.addCode(protocolSpec.executionHandler(opModel))
.build());

// Add extra methods for paginated operations
if (model.getPaginators().keySet().contains(opModel.getOperationName())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this to it's own method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

.addModifiers(Modifier.DEFAULT)
.addStatement("throw new $T()", UnsupportedOperationException.class)
.build());

methods.addAll(streamingSimpleMethods(opModel));

// Add extra methods for paginated operations
if (model.getPaginators().keySet().contains(opModel.getOperationName())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move code to its own method


public SdkIterable&lt;String> tableNames() {
Function&lt;ListTablesResponse, Iterator&lt;String>> getPaginatedMemberIterator = response -> response != null
? response.tableNames().iterator() : null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If response is null, I am returning null. This is used in PaginatedItemsIterable which checks has a check to handle null values. Are you suggesting to use empty iterator?

@varunnvs92 varunnvs92 force-pushed the varunkn/SyncPagination branch from ab338c2 to d70afcb Compare October 27, 2017 18:53
@varunnvs92 varunnvs92 changed the title Sync pagination implementation to get initial feedback Sync pagination implementation Oct 27, 2017
@varunnvs92
Copy link
Contributor Author

Made changes based on spfink@ feedback

@varunnvs92 varunnvs92 force-pushed the varunkn/SyncPagination branch from d70afcb to b7fe511 Compare October 31, 2017 18:14
.build());

TableUtils.waitUntilActive(dynamo, TABLE_NAME);
// new DynamoDBClientWaiters(dynamo)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove the commented-out codes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

@JsonProperty("limit_key")
private String limitKey;

public PaginatorDefinition() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default constructor probably not required.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the code style we follow for classes with just default constructor.

*/
public class Paginators {

public static final Paginators NONE = new Paginators(Collections.emptyMap());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe rename it to EMPTY_PAGINATOR or NONE_PAGINATOR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a fan of NONE since it's clear from the qualifier, Paginators.NONE


package software.amazon.awssdk.codegen.utils;

public class PaginatorUtils {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final on class and private constructor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -33,7 +33,8 @@ public AwsGeneratorTasks(GeneratorTaskParams params) {
}

private Iterable<GeneratorTask> createAwsTasks(GeneratorTaskParams params) {
return new CompositeIterable<>(new AsyncClientGeneratorTasks(params));
return new CompositeIterable<>(new AsyncClientGeneratorTasks(params),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a TODO for the async generator tasks. It's something we'll defintely wanna push up to the generic tasks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -43,6 +43,8 @@
MAPPER.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
// TODO: Un comment this once we know for sure, we capture everything in C2j model.
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// TODO: Risky to enable for all deserialiation?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, removed the TODO. Thanks

*/
public class Paginators {

public static final Paginators NONE = new Paginators(Collections.emptyMap());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a fan of NONE since it's clear from the qualifier, Paginators.NONE

*
* Sample of a generated class with annotations:

public final class ListTablesPaginator implements Paginated&lt;ListTablesResponse, String> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably would remove this from the javadocs and just have it in the diff test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, what is diff test? Do you mean a new test class to test the generation of sample paginators file?

package software.amazon.awssdk.pagination;

@FunctionalInterface
public interface NextPageSupplier<ResponseT> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels more internal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

@SdkProtectedApi
public class PaginatedItemsIterable<ResponseT, ItemT> implements SdkIterable<ItemT> {

private final Paginated paginator;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have some kind of generics?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. I actually removed the Paginated interface as we are removing the main allItems() method from it. Not the response class directly extends SdkIterable instead of Paginated.
Ex:
public final class ListTablesPaginator implements SdkIterable
instead of
public final class ListTablesPaginator implements Paginated<ListTablesResponse, String>

Let me know if you have any concerns?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

throw new NoSuchElementException();
}

// Using while loop here to handle response pages with empty collection of items
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to handle the case you mentioned that there can be pages with empty collection of items. In this code, we retrieve the next page until there is a next element in itemsIterator or no more response pages.
If there is next element in itemsIterator, we return it.
If there is no response page, it means there are no more elements to return and so we return null. I am not satisfied with this. But I don't like throwing error as hasNext() before this next() call would have returned true.



if (!itemsIterator.hasNext()) {
// TODO throw error or return null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somethings fishy here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment. I am not satisfied with it too. This happens only in cases with empty item collection in last page. I don't see a good way to handle it. Any ideas?

*/
public class PaginatedResponsesIterator<ResponseT> implements Iterator<ResponseT> {

private final NextPageSupplier<ResponseT> nextPageSupplier;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supplier makes me think of a factory. This is really more of a function. NextPageFetcher?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this is a function. Based on Dongie's comment to have better naming instead of apply(), created this NextPageSupplier functional interface. I am okay with both names.

@aws/aws-java-sdk
Anyone else want to chime in their opinion with naming - NextPageSupplier vs NextPageFetcher?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NextPageFetcher +1


ResponseT oldValue = currentResponse;

currentResponse = nextPageSupplier.nextPage(currentResponse);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preemptive fetch seems weird to me. I'm hestitant to make service calls the customer didn't really ask for.

Copy link
Contributor

@kiiadi kiiadi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want a change log entry for this too since it's a sizable feature.

@Override
public ResponseT next() {
if (!hasNext()) {
throw new NoSuchElementException();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a message here like "No more pages left".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated


@Override
public boolean hasNext() {
return (singlePageItemsIterator != null && singlePageItemsIterator.hasNext()) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think to preserve the semantics of Iterator interface you need to do the loop here. You can't know if there are more items just by the presence of another page.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats true. We have to retrieve the next page to verify that right? But we discussed that we should not make service calls in hasNext() method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Items iteration I can't see another way to do it. I think it's more acceptable in that case since you have given up control of when service calls are made already (i.e. there is no contract of where page boundaries are and when service calls are made). If there's a way to not make that call and still preserve the Iterator contract I'm all for it, but I don't think thats possible.

* @see <a href="http://docs.aws.amazon.com/goto/WebAPI/json-service-2010-05-08/PaginatedOperationWithoutResultKey"
* target="_top">AWS API Documentation</a>
*/
default PaginatedOperationWithoutResultKeyPaginator paginatedOperationWithoutResultKeyIterable(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going with the Iterable naming can we change Paginator to just Iterable? I.E. ListTablesIterable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had same idea but thought keeping paginator in return type will be more clear to customer that it is a paginated (or atleast some special) API.

import software.amazon.awssdk.services.jsonprotocoltests.model.SimpleStruct;

/**
* Represents the output for the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is some really nice Javadoc! Would it be more discoverable on the operation method itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add docs in client too. Was figuring out some formatting issues

* guarantee that your request is valid. As you iterate through this iterable, SDK will lazily load response pages by
* making service calls until there are no pages left or your iteration stops.
*
* Following are few ways to iterate through the response pages:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following
Here are

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

if (oldPage == null) {
return client.paginatedOperationWithResultKey(firstRequest);
} else {
return client.paginatedOperationWithResultKey(firstRequest.toBuilder().nextToken(oldPage.nextToken()).build());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor but perhaps just a ternary here.

return client.paginatedOperationWithResultKey(firstRequest.toBuilder()
      .nextToken(oldPage == null ? null : oldPage.nextToken())
     .build();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output_token is a list i.e., there might be multiple values in response that is needed to get the next page. I feel ternary becomes convoluted to read and i prefer to keep the if-else.

return client.paginatedOperationWithResultKey(firstRequest.toBuilder()
      .nextToken1(oldPage == null ? null : oldPage.nextToken1())
      .nextToken2(oldPage == null ? null : oldPage.nextToken2())
     .build();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, if using if-else, It might be cleaner to remove the else.

if (oldPage == null) {
      return client.paginatedOperationWithResultKey(firstRequest);
}

return client.paginatedOperationWithResultKey(firstRequest.toBuilder().nextToken(oldPage.nextToken()).build());

* {@link JsonProtocolTestsClient#paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest)} API.
*/
@Generated("software.amazon.awssdk:codegen")
public final class PaginatedOperationWithResultKeyPaginator implements SdkIterable<PaginatedOperationWithResultKeyResponse> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there are two of these?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep was aware of it. Some issue with loading the file in paginators package. Will fix it.


private final NextPageFetcher nextPageFetcher;

public PaginatedOperationWithoutResultKeyPaginator(final JsonProtocolTestsClient client,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there are two of these?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above.


public class PaginatedItemsIterableTest {

private SdkIterable pagesIterable = Mockito.mock(SdkIterable.class);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do the RunWith mockito runner you can annotate these with just Mock

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh thats it. I tried with @mock annotations but I was getting NPE. I forgot to annotate the class with MockitoJUnitRunner.

}

@Test
public void nextMethodDoesnotRetrieveNextPage_WhenItemsIteratorHasAnElement() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DoesNot

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

@varunnvs92 varunnvs92 force-pushed the varunkn/SyncPagination branch 4 times, most recently from 8aa0122 to af55b8d Compare November 8, 2017 17:57
/**
* Constructs additional documentation on the client operation that is appended to the service documentation.
*/
public String getDocsForSyncOperation() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a TODO here to link to our developer guide. We'll probably want to go into a little more depth in the guide so a link to it when available would be nice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

.addAnnotation(Override.class)
.addCode(getCustomResponseHandler(opModel, returnType)
.orElseGet(() -> protocolSpec.responseHandler(opModel)))
.addCode(protocolSpec.errorResponseHandler(opModel))
.addCode(protocolSpec.executionHandler(opModel))
.build());

// Add extra methods for paginated operations
if (opModel.isPaginated()) {
methods.add(SyncClientInterface.operationMethodSignature(model, opModel,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If new lining, we typically new line all arguments

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

.addAnnotation(Override.class)
.returns(poetExtensions.getResponseClassForPaginatedSyncOperation(opModel.getOperationName()))
.addStatement("return new $T(this, $L)",
poetExtensions.getResponseClassForPaginatedSyncOperation(opModel.getOperationName()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line these lines up?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

.addModifiers(Modifier.DEFAULT)
.addStatement("throw new $T()", UnsupportedOperationException.class)
.build());

methods.addAll(streamingSimpleMethods(opModel));

// Add extra methods for paginated operations
if (opModel.isPaginated()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move all specific logic to other methods?

Basically end up with:

methods.addAll(paginatedSimpleMethods());
methods.addAll(paginatedMethods());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we want to have separate methods that go through all operations agains to find the paginated operations.

I moved the new code to a new method that takes in an operation model and return the method specs if its paginated operation.
List paginatedMethods(OperationModel opModel)

the call will end up with
methods.addAll(paginatedMethods(opModel));

private static final String NEXT_PAGE_FETCHER_MEMBER = "nextPageFetcher";
private static final String HAS_NEXT_PAGE_METHOD = "hasNextPage";
private static final String NEXT_PAGE_METHOD = "nextPage";
private static final String OLD_PAGE_METHOD_ARGUMENT = "oldPage";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous page?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

public PaginatorResponseClassSpec(IntermediateModel intermediateModel,
String c2jOperationName,
PaginatorDefinition paginatorDefinition) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra new line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

@@ -82,14 +90,14 @@ protected DefaultJsonAsyncClient(AsyncClientConfiguration clientConfiguration) {
public CompletableFuture<APostOperationResponse> aPostOperation(APostOperationRequest aPostOperationRequest) {

HttpResponseHandler<APostOperationResponse> responseHandler = protocolFactory.createResponseHandler(
new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the changes in formatting throughout this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied the expected file when the unit test failed. Then checked if new code looks as expected. I guess the generated file in unit test has different formatting. I tried to auto-format the file now which is having lot more changes

* of this type.</li>
* </ul>
* @sample JsonAsyncClient.PaginatedOperationWithResultKey
* @see <a href="http://docs.aws.amazon.com/goto/WebAPI/json-service-2010-05-08/PaginatedOperationWithResultKey"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this link work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is generated for all operations in this class. All of them redirect to AWS documentation official page.

}

/**
* Some paginated operation with result_key in paginators.json file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you generated the javadoc and made sure this all looks good?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, looks good

@Test
public void test_MultipleIteration_On_Responses_Iterable() {
ScanRequest request = ScanRequest.builder().tableName(TABLE_NAME).limit(2).build();
ScanPaginator scanResponses = dynamo.scanIterable(request);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing it definitely don't know if I like "scanIterable"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we will go with "Iterable". We would want to have consistent naming across sync and Async clients. Based on #185, Iterable doesn't make sense for Async operations. So we need change them for sure.

@spfink
Copy link
Contributor

spfink commented Nov 9, 2017

Added a few comments. Mostly minor stuff. Def need to figure out naming.

@varunnvs92 varunnvs92 force-pushed the varunkn/SyncPagination branch 2 times, most recently from f39cc29 to 3930680 Compare November 10, 2017 23:33
Copy link
Contributor

@spfink spfink left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine with where this is now. We might make changes as we start to using the paginators (probably in naming) but think this is good to ship out

@varunnvs92 varunnvs92 force-pushed the varunkn/SyncPagination branch 5 times, most recently from 5f024f6 to 3d360ee Compare November 14, 2017 01:39
@varunnvs92 varunnvs92 force-pushed the varunkn/SyncPagination branch from 3d360ee to dc0a9cc Compare November 14, 2017 01:46
@varunnvs92 varunnvs92 merged commit 75c1ba7 into master Nov 14, 2017
@zoewangg zoewangg deleted the varunkn/SyncPagination branch December 6, 2017 19:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants