Skip to content
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

Change CacheControl to Interceptor based Fetcher #556

Merged
merged 12 commits into from
Jul 18, 2017

Conversation

BenSchwab
Copy link
Contributor

@BenSchwab BenSchwab commented Jul 10, 2017

Motivation

The interceptor architecture we use for executing a request is loosely similar to the future direction Apollo JS is going: https://github.com/apollographql/apollo-fetch. If I understand correctly, the vision is that Apollo Fetch will make it easy to write fetch policies like CACHE_FIRST or more complicated policies like CACHE_FIRST_IF_NOT_STALE_BUT_IF_NO_NETWORK_ALLOW_STALE.

However, shipping with every possible fetch strategy is impossible -- thus our current CacheControl approach based on a set of enums, could be made more flexible. This PR proposes making CacheControl an ApolloInterceptor that contains logic of how to fetch the request. (And changes the name to Fetcher to better align with JS).

Fetcher

  • Just an ApolloInterceptor
  • Uses FetchOptions to configure fetching preferences
  • Can change CacheHeaders and a boolean fetchFromCache to control how the data will be fetched
  • Thus CACHE_FIRST is implemented by first proceeding the chain with a FetchOptions with fetchFromCache = true and catching an exception. If exception it will try to proceed with fetchFromCache = false

Benefits

  • ApolloCacheInterceptor has less logic - all fetching logic is contained in the Fetcher
  • Fairly easy to implement a custom Fetcher for specific needs
  • In basic use case, API is no more complicated
 githuntFeedCall = application.apolloClient()
        .query(feedQuery)
        .fetcher(ApolloFetchers.CACHE_FIRST);

Mutli-response

Notably this PR enables a fetcher to respond multiple times. The rationale behind this is that when you use Apollo with a persistent sql cache, currently the only fetch policy that works is NETWORK_FIRST which negates the value of the sql / in memory cache. This is because if you use CACHE_FIRST, after the first cache population, the data will never be automatically refreshed. This could be handled with some sort of "staleness" system, but again, this would result in the cache data always being held until it is stale. Rather, it would be useful to allow a request to fulfill from cache first, and then also, fetch updated data from the network.

This does complicate the callback API, particularly with ApolloRx, as we need to know when a request will return no more data (onCompleted).

This PR ships with one double response fetch policy CACHE_AND_NETWORK.

Additional work

  • Integrate with ApolloSupport
  • Add tests
  • Add documentation
  • Make sure cancel and dispose works properly

Previous discussions: https://airbnb.quip.com/BVbnAztIGoLm

closes #432

@BenSchwab
Copy link
Contributor Author

cc @kompfner @martijnwalraven for thoughts from iOS perspective

@BenSchwab
Copy link
Contributor Author

@sav007 @digitalbuddha Let me know what you think about going in this direction, if things seem good from a conceptual perspective, I'll move forward with writing all the tests, documentation, and code cleanup

@sav007
Copy link
Contributor

sav007 commented Jul 10, 2017

I love this idea, clean and flexible


public interface Fetcher {

ApolloInterceptor create(ApolloLogger logger);
Copy link
Contributor

Choose a reason for hiding this comment

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

should it be renamed for clarity to smth provideInterceptor?

import com.apollographql.apollo.interceptor.ApolloInterceptor;
import com.apollographql.apollo.internal.util.ApolloLogger;

public interface Fetcher {
Copy link
Contributor

@sav007 sav007 Jul 10, 2017

Choose a reason for hiding this comment

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

Should it be called ResponseFetcher?

@digitalbuddha
Copy link
Contributor

I like the concept but am not crazy with the builder method name

.fetcher(ApolloFetchers.CACHE_FIRST) feels weird. It's not a fetcher but a cache policy or a fetch policy. When I see a builder method called fetcher I expect to pass in something that has an implementation for fetch.

@digitalbuddha
Copy link
Contributor

Ok next time I'll look at code and not just description of pr. It does fetch! I'm with Ivan love it.


private synchronized void handleNetworkError(ApolloException exception) {
networkException = Optional.of(exception);
dispatch();
Copy link
Contributor

Choose a reason for hiding this comment

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

should we consider to not dispatch if result if it was canceled (disposed)

@sav007
Copy link
Contributor

sav007 commented Jul 11, 2017

We need to make sure that cancel on RealApolloCall works as expected, all chained interceptors are been disposed

@BenSchwab
Copy link
Contributor Author

BenSchwab commented Jul 12, 2017

Divided this into runtime changes, rx changes, and test changes.

Also, took a stab at #560 as this PR required me to address cancellation in the new fetchers.

To avoid the memory leaks #560 during cancelation a made the callback a WeakReference.

I don't think the WeakReference approach is a good idea. It as causing issues in tests, and probably could surface other issues. Let's fix 560 in a separate PR to avoid doing too much here.

There is a slight change in behavior of cancellation:

  1. If cancel is called before enqueue or execute, there will be an exception
  2. If cancel is called during execute, there will be an exception
  3. If cancel is called during enqueue there will be no exception, and callback will not be triggered

The change is in point (3) which would previously trigger onError. This felt weird to me, as if I call cancel I don't expect any callbacks (Rx follows this model).

I also more aggressively check if dispose has been called on interceptors to avoid doing unnecessary work.

@BenSchwab BenSchwab force-pushed the bschwab--interceptor-fetch-policy branch 2 times, most recently from ba6a547 to 0660a7c Compare July 12, 2017 22:05
@BenSchwab BenSchwab changed the title [WIP] Change CacheControl to Interceptor based Fetcher Change CacheControl to Interceptor based Fetcher Jul 12, 2017
@BenSchwab BenSchwab force-pushed the bschwab--interceptor-fetch-policy branch 2 times, most recently from f2547db to 5595cb0 Compare July 13, 2017 17:35
@BenSchwab BenSchwab force-pushed the bschwab--interceptor-fetch-policy branch 4 times, most recently from ae780cf to c7af7cc Compare July 14, 2017 16:38
import com.apollographql.apollo.internal.fetcher.NetworkFirstFetcher;
import com.apollographql.apollo.internal.fetcher.NetworkOnlyFetcher;

public final class ApolloResponseFetcher {
Copy link
Contributor

@sav007 sav007 Jul 15, 2017

Choose a reason for hiding this comment

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

nit: should it be named ApolloResponseFetchers, like in java Executors, in Rx Schedulers?

public final boolean fetchFromCache;
public final CacheHeaders cacheHeaders;

public static final FetchOptions NETWORK = new FetchOptions(false, CacheHeaders.NONE);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: NETWORK_ONLY?

@sav007
Copy link
Contributor

sav007 commented Jul 15, 2017

Just curios if FetchOptions is really necessary, is behaviour inside the chained interceptors provided by ResponseFetcher is not enough?

some code review changes

fix runtime checkstyle

runtime fix

remove weak reference changes

runtime changes
remove unused test

test fixes

test changes

mend
@BenSchwab BenSchwab force-pushed the bschwab--interceptor-fetch-policy branch from c7af7cc to f47f63c Compare July 15, 2017 21:07
@BenSchwab
Copy link
Contributor Author

While, I believe it would be possible to write a custom Fetcher (ApolloInterceptor) that could resolve from network / cache, ideally user's can just proceed with the combo of CacheInterceptor/ParseInterceptor/ServerIntercept that we already assemble. It seemed to me the easiest way to pass a configuration to CacheInterceptor to instruct it how to proceed is an options object with fetchFromCache and CacheHeaders both of which many custom fetch strategies would change to achieve different effects (adding a CacheHeader like "ALLOW_STALE" after an initial cache and network miss)

@sav007
Copy link
Contributor

sav007 commented Jul 16, 2017

We should update java doc for Callback: public abstract void onResponse(@Nonnull Response<T> response); to mention that this can be called multiple times, user should be prepared for this

@sav007
Copy link
Contributor

sav007 commented Jul 16, 2017

And we need to mention that Callback.onCompleted won't be called in case of error

*
* @param cacheControl the CacheControl strategy to set
* @param fetcher the {@link ResponseFetcher} to use.
* @return The ApolloCall object with the provided CacheControl strategy
Copy link
Contributor

Choose a reason for hiding this comment

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

@return The ApolloCall object with the provided CacheControl strategy ->
@return The ApolloCall object with the provided fetch strategy

Copy link
Contributor

@sav007 sav007 left a comment

Choose a reason for hiding this comment

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

Just couple comments on JavaDocs. Other than that lgtm

return this;
}

/**
* Set the default {@link CacheHeaders} strategy that will be passed
* Set the default {@link ResponseFetcher} that will create a {@link ResponseFetcher} to be passed
Copy link
Contributor

Choose a reason for hiding this comment

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

smth wrong here ResponseFetcher will create a ResponseFetcher

@@ -66,30 +63,39 @@ private Rx2Apollo() {
}

/**
* Converts an {@link ApolloCall} to a synchronous Single.
* Converts an {@link ApolloCall} to an {@link Observable}.
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess we should provide more info why it's Observable on cache and network fetch strategy example

@@ -81,30 +79,50 @@ private RxApollo() {
}

/**
* Converts an {@link ApolloCall} to a Single.
* Converts an {@link ApolloCall} to a Observable.
Copy link
Contributor

Choose a reason for hiding this comment

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

the same as above

@BenSchwab
Copy link
Contributor Author

@sav007 I guess I should bump the version number again since this is a breaking change? Not sure we have enough new stuff since that last release to publish an official release

@sav007
Copy link
Contributor

sav007 commented Jul 17, 2017

but we have't released 0.4.0 yet, why do we need to bump it to 0.5.0?

@BenSchwab
Copy link
Contributor Author

Ok, I wasn't sure if we were trying to keep snapshot builds being semver capatabilble. But if not, let's wait to bump!

@BenSchwab BenSchwab merged commit caaae7e into master Jul 18, 2017
@BenSchwab BenSchwab deleted the bschwab--interceptor-fetch-policy branch July 18, 2017 01:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cache Fetch Strategy / Cache Header unification proposal
3 participants