-
Notifications
You must be signed in to change notification settings - Fork 751
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
Get Implementation in AsyncCache that takes in a callback function #344
Conversation
Hi Scott, I'd recommend to update the PR title to be a bit more descriptive. I find that the first sentence of your description provides much more context on what this PR is about, so feel free to use it as the title. Descriptive titles are important is it allows to quickly get a sense of what the PR is about. |
import java.util.concurrent.CountDownLatch; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
|
||
public abstract class AsyncCache implements Cache { |
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'd recommend to add a descriptive doc on how to use this cache implementation. It has a very different intended usage over the original interface (e.g. it's expected to use the async get/put/etc. with a callback rather than using what the Cache
interface defines), so it's worth clarifying in the class documentation.
Here's a link to how it's recommended to write javadoc for Google projects: https://google.github.io/styleguide/javaguide.html#s7-javadoc
In addition to adding top-level class javadoc, could you document each public interface, class, methods, etc. that are part of this file? It's OK not to write doc for what's define by the Cache
interface, though I'd still suggest to add a note to use the async methods instead.
final AtomicReference<Entry> entryRef = new AtomicReference<>(); | ||
get( | ||
key, | ||
new OnGetCompleteCallback() { |
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.
nit: we could use a lambda expression here for simplicity (I believe all version of Android can now use a lambda when using a recent Android Gradle plugin)
This would convert your code into:
get(
key,
entry -> {
entryRef.set(entry);
latch.countDown();
}
);
For context, lambdas are like a more concise way to write an anonymous class. Feel free to take a look at the official java doc about lambdas if it's the first time you hear about them / use them: https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
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.
When I tried to make that change, it says lambda expressions aren't allowed in Java 7. Even though I thought Volley uses java8. I'll look into 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.
Volley actually is built with minSdkVersion = 8, which is actually Java 6 :)
Lambdas have been backported in principle per https://developer.android.com/studio/write/java8-support - not sure offhand how far back this goes. But I'm not 100% sure what the story is offhand in terms of exposing a library that relies on this support.
While it's not ideal, unless we can pretty quickly and confidently find an answer suggesting we can safely use lambdas without introducing major complexity for apps depending on the library using this desugaring support, we should probably avoid them except when writing code that is guaranteed to run on the right minSdkVersion.
Note that this also includes try-with-resources.
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.
FWIW, I'm open to considering increasing the minSdkVersion somewhat. At this point it's exceedingly rare to target anything under API 14, which is what the Android support library requires; that seems like a reasonable point to increase it, if that'd be helpful. But it'd be nice to have a clear benefit to doing so, and 14 won't get us native try-with-resources or lambdas AFAIK.
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.
To be clear, the current Java target will support try-with-resources if sdk >= 19
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
Though I think try-with-resources can obscure what's going on, esp. in nested and other complex situations, so no loss of any significance.
https://stackoverflow.com/a/19470405/901597
Java 8 is needed for lambda.
// TODO: Handle callback never being invoked? Match behavior when sync cache throws | ||
// RuntimeException. | ||
|
||
@Override |
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.
Could you also annotate the method as @Nullable
? It acts a documentation to tell the caller that the return value could be 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.
fixed
public abstract class AsyncCache implements Cache { | ||
|
||
public interface OnGetCompleteCallback { | ||
void onGetComplete(Entry entry); |
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.
Since entry
can be null, please annotate the parameter as @Nullable
, so whoever implements the class will be aware that the method parameter can be 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.
fixed
} | ||
} | ||
|
||
// TODO: Implement the rest. |
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.
nit: did you mean to move this below put
?
cb.onGetComplete(null); | ||
} | ||
}); | ||
} catch (Exception e) { |
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 DiskBasedCache
only catchtes IOException
s. Is there a reason why we want to catch any Exception
s in 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.
Oh no, I think I just put that there when I first wrote the try/catch, and never changed it. I'll fix that now.
|
||
/** Handles holding onto the cache headers for an entry. */ | ||
@VisibleForTesting | ||
static class CacheHeader { |
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.
Could CacheHeader
be shared between this class and DiskBaseCache
? The implementations don't look the same at this time, but just wondering what's the long term plan and if it would make sense to extract the class to a top-level class so it can be shared.
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.
Yeah, I was thinking about making another abstract class that these two extend. This would allow us to not repeat the shared methods/class. Currently, I think the write and read methods would be implemented differently, so I don't know how that'd work. But I think that's definitely better to try to do that.
import java.util.List; | ||
import java.util.Map; | ||
|
||
public class DiskBasedAsyncCacheFallback extends AsyncCache { |
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.
Could you also add doc here to explain how the class differs from DiskBasedAsyncCache
?
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
File file = getFileForKey(key); | ||
|
||
try { | ||
DiskBasedAsyncCacheFallback.CountingInputStream cis = |
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 believe you can use a try-with-resources here as well.
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 says that it requires an API of 19 or higher to do a try-with-resources.
File file = getFileForKey(key); | ||
|
||
try { | ||
DiskBasedAsyncCacheFallback.CountingInputStream cis = |
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.
nit: I think you may not need to add the DiskBaseAsyncCacheFallback
prefix since it's an inner class of this class. (same below)
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, just fixed this.
|
||
public abstract void get(String key, OnGetCompleteCallback callback); | ||
|
||
// TODO: Handle callback never being invoked? Match behavior when sync cache throws |
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.
nit: Add the issue in parentheses:
// TODO(#181): Handle...
@Override | ||
public void get(String key, OnGetCompleteCallback callback) { | ||
final OnGetCompleteCallback cb = callback; | ||
final DiskBasedAsyncCache.CacheHeader entry = mEntries.get(key); |
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.
Check that entry is non-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.
fixed
afc.read( | ||
buffer, | ||
0, | ||
buffer, |
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.
Add args to these two to clarify what they are:
/* position= / 0,
/ attachment= */ buffer
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 those
// TODO (sphill99): Implement | ||
} | ||
|
||
/** |
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 think some of these common functions that are the same between your new Cache implementations and the existing one could be factored out into a Util that all three classes could call
You may be able to do the same with some of the CONSTANTS at the top of the file that seem to be the same for all three
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.
Yeah, I want to extract some of the stuff that's repeated out to a higher level class. Should I do that for this PR?
@joebowbeer I still haven't written tests for this function. I'm going to write tests for the AsyncCache as a whole, including this implementation. I just wanted to get the general structure reviewed before I move further with the implementations. Currently these implementations aren't being used. I'll make sure to keep the existing coverage. |
AsynchronousFileChannel.open(path, StandardOpenOption.READ); | ||
ByteBuffer buffer = ByteBuffer.allocate((int) file.length()); | ||
afc.read( | ||
/* destination */ buffer, |
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.
nit: add "="
e.g. /* destination= */ buffer
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 maximum size of the cache in bytes. */ | ||
private final int mMaxCacheSizeInBytes; | ||
|
||
/** Default maximum disk usage in bytes. */ |
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.
Are these used anywhere yet? If not, I think hold off on including them until they're used.
When you do need to add them, I think you can put them in your new Utility class since they are the same for both implementations (async and fallback)
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 removed these for now
|
||
/** Returns the cache entry with the specified key if it exists, null otherwise. */ | ||
@Override | ||
public synchronized void get(String key, OnGetCompleteCallback callback) { |
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 really similar to the existing DiskBasedCache, except instead of returning null or entry.toCacheEntry, you call callback.onGetComplete(null/entry.toCacheEntry)
I'm wondering if it would be possible to use the existing DiskBasedCache in here and just call #get, then do callback.onGetComplete with whatever it returns. There may be a couple ways of approaching this, so happy to discuss more offline too.
I think it would generally simplify this class if you can leverage the existing implementation.
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.
+1 - for the fallback, we should try to share as much code with possible as the existing implementation rather than duplicating it.
In fact, though, I wonder if we really want to have a second class at all. The reality is that it's confusing to have a class extend AsyncCache but not actually be Async - how should the user of this object determine whether the cache supports true Async operation (and thus doesn't need to be called on a blocking thread) or not (and thus does)?
The class which uses this will ultimately need to make a decision based on the capabilities of the cache to either use the fully async API or else send the task to a blocking thread pool. But in the latter case, there's actually no reason to use the async API methods, since they're blocking anyway.
This might become clearer when we actually implement the async cache handling step, but my instinct here is that we actually don't need this class, and should instead have the dispatcher accept either a vanilla Cache (which defaults to DiskBasedCache) or an AsyncCache (which defaults to DiskBasedAsyncCache, where supported). If the cache is an instance of AsyncCache, it knows it can safely use the async methods. If not, it should use the fallback and schedule the read for a blocking background thread.
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 changed it so the fallback now doesn't repeat the code. But yeah, that makes sense.
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.
That's certainly better, but then why would we need it at all per my comment? Would it make sense to hold off on adding it unless/until we see how it would be helpful/needed, or do you see a reason that the next level up would make use of this, given that it will need to know how to dispatch the cache operations?
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.
oh okay, I understand what you're saying now. I just deleted it in the last commit.
|
||
@Nullable | ||
@Override | ||
public Entry get(String key) { |
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 think we should mark the legacy methods as final - subclasses of AsyncClass should not be modifying these implementations, only implementing the async versions. (At least until someone can provide a valid use case for it; until then, the tighter we keep the API, the stronger the guarantees the implementation can offer).
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
try { | ||
latch.await(); | ||
return entryRef.get(); | ||
} catch (InterruptedException e) { |
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.
We should probably log the exception here as well (with VolleyLog). Otherwise, the operation might not have gone through, but apart from the thread's interrupt status (which is often unchecked by callers), there'd be no visible indication that something had been canceled.
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.
Consider awaitUninteruptibly. Is this thread exposed enough to be interrupted? If not, this handling is just another section of untested code.
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.
awaitInterruptibly isn't a method on CountDownLatch.
While I agree interrupt handling is a mess in Java, I think we can at least ensure we handle them correctly to the extent possible within Volley (we do have RequestQueue#stop which would stop the executors if we control them).
|
||
/** Returns the cache entry with the specified key if it exists, null otherwise. */ | ||
@Override | ||
public synchronized void get(String key, OnGetCompleteCallback callback) { |
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.
+1 - for the fallback, we should try to share as much code with possible as the existing implementation rather than duplicating it.
In fact, though, I wonder if we really want to have a second class at all. The reality is that it's confusing to have a class extend AsyncCache but not actually be Async - how should the user of this object determine whether the cache supports true Async operation (and thus doesn't need to be called on a blocking thread) or not (and thus does)?
The class which uses this will ultimately need to make a decision based on the capabilities of the cache to either use the fully async API or else send the task to a blocking thread pool. But in the latter case, there's actually no reason to use the async API methods, since they're blocking anyway.
This might become clearer when we actually implement the async cache handling step, but my instinct here is that we actually don't need this class, and should instead have the dispatcher accept either a vanilla Cache (which defaults to DiskBasedCache) or an AsyncCache (which defaults to DiskBasedAsyncCache, where supported). If the cache is an instance of AsyncCache, it knows it can safely use the async methods. If not, it should use the fallback and schedule the read for a blocking background thread.
private final int mMaxCacheSizeInBytes; | ||
|
||
/** Default maximum disk usage in bytes. */ | ||
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024; |
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 share magic constants like this with DiskBasedCache at least until we need to fork them? e.g. by putting them in DiskBasedCacheUtility
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
@Override | ||
public void completed(Integer result, ByteBuffer attachment) { | ||
if (attachment.hasArray()) { | ||
int offset = attachment.arrayOffset(); |
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.
nit: looks like this variable is unused
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.
For some reason, you can't call attachment.array() without having called hasArray() and arrayOffset() beforehand. Some bytebuffers don't have a backing array, but since we always pass one in that does, it always will. I'm not sure why simply checking if it hasArray doesn't suffice, but it doesn't build otherwise.
/* attachment */ buffer, | ||
new CompletionHandler<Integer, ByteBuffer>() { | ||
@Override | ||
public void completed(Integer result, ByteBuffer attachment) { |
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.
wondering whether for safety, we need to read the value of result and truncate the entry to that size (/ handle if it is -1, per the Javadoc).
With the old cache, we'd occasionally see strange bugs due to files being corrupted, possibly due to parallel access. While that's a misuse of Volley if so, it is good if we can gracefully fall back to the extent possible. And it's possible the size changes between when we read the length / allocate the buffer and when we complete the actual data read. So the buffer might have garbage data at the end (if the file was truncated), or might be incomplete (if it grew). In both cases we should probably assume the entry is invalid and avoid returning it.
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 added a check to see if the file length is the same as the size allocated before the read.
} | ||
|
||
@Override | ||
public void failed(Throwable exc, ByteBuffer attachment) { |
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.
log the exception so the failure is visible
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
} | ||
}); | ||
} catch (IOException e) { | ||
cb.onGetComplete(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.
log exception
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
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.
better, but see above note regarding how to handle errors
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.
Line 106 is still potentially a second invocation of callback.
See comment above, and this example of another way to structure this:
I should have made appropriate edits for all the PR comments, or left questions on them. It is ready to be reviewed again. |
/** | ||
* The size of the data identified by this CacheHeader on disk (both header and data). | ||
* | ||
* <p>Must be set by the caller after it has been calculated. | ||
* | ||
* <p>This is not serialized to disk. | ||
*/ |
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 comment looks intended for long size;
below
final String key; | ||
|
||
/** ETag for cache coherence. */ | ||
final String etag; |
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.
nit: please annotate as @Nullable
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
File file = getFileForKey(key); | ||
Path path = Paths.get(file.getPath()); | ||
try { | ||
AsynchronousFileChannel afc = |
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.
Since we can't use try-with-resources. make sure to close the file channel when the try/catch block is completed
public void completed(Integer result, ByteBuffer attachment) { | ||
// if the file size changes, return null | ||
if (size != file.length()) { | ||
cb.onGetComplete(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.
maybe log a warning in this case since it implies the file changed while we were reading it.
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
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 this mean that the read terminated early? I would treat this as a failure.
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 think it means the file changed between the time we opened the channel and finished the reading. The size allocated and the file length shouldn't be affected by the success of the read. I think if the read fails, it would never reach the completed block.
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 either case, it sounds like an error to me. I don't think it serves much purpose to check the file length if it will be ignored for all practical purposes. I would either ignore the file length and use the bytes read, if this is reliable, or use the file length as a correctness check, if not.
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 think the confusion here is - this should say "if (size != result)". We shouldn't compare the file size before and after - we should compare the size of the file when we allocated the buffer to the number of bytes we actually read.
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.
but the number of bytes we actually read is dependent upon the size of the buffer we allocate. This would work in the case of the read bytes being less than the allocated, but it will never be more.
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.
true. but we should still look at the number of bytes read and make sure it's not less, which could happen even if the file size is now the same.
I think checking for result != size || current file size != size would be reasonable. Or, just the former, and let the latter be handled by integrity checks on the cache header.
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.
Okay, I'll make that fix
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.
Fix potential second callback.
afc.close(); | ||
} catch (IOException e) { | ||
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString()); | ||
cb.onGetComplete(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.
Here you are calling the callback a second time.
Consider a nested try-finally try-catch. One to make sure afc is closed and one to callback on IOException. There may be an example of this pattern in the disk based cache.
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.
oh thanks, just fixed
I think I addressed all PR comments. It is ready to be reviewed again. |
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.
Thanks - most of my comments now are about rejiggering small pieces rather than major architectural changes, so I think this is getting closer. Once we stamp out the general patterns here, hopefully the remaining methods won't need a lot of round trips.
try { | ||
latch.await(); | ||
return entryRef.get(); | ||
} catch (InterruptedException e) { |
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.
awaitInterruptibly isn't a method on CountDownLatch.
While I agree interrupt handling is a mess in Java, I think we can at least ensure we handle them correctly to the extent possible within Volley (we do have RequestQueue#stop which would stop the executors if we control them).
new OnPutCompleteCallback() { | ||
@Override | ||
public void onPutComplete(boolean success) { | ||
if (success) { |
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.
AFAICT we need to count down the latch regardless of whether the put succeeds or fails. If we don't, the caller will just be blocking indefinitely even though nothing is still happening.
put doesn't provide a way to return an error, so we should make sure it's logged somewhere in the stack, but otherwise we should just return control to the caller once the put is done.
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 it be logged in this method, or in the async implementation, where we know what happened?
|
||
/** | ||
* AsyncCache implementation that uses Java NIO's AsynchronousFileChannel to perform asynchronous | ||
* disk reads and writes. This should only be used by devices with an API level of 26 or above. |
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 think you can remove this sentence and the @SuppressLint
annotation and just add a @RequiresApi(Build.VERSION_CODES.O)
instead. This is preferable because:
- Instead of suppressing all lint checks, it only disables them for APIs added up through O. If you make an API call to something added in P, it will still fail, which is good as we would need additional checks.
- It will enforce that the caller checks for O+ before using the class (or that it has a RequiresApi annotation of its own)
- Its self-documenting - no need to repeat it in the 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.
Fixed this
import java.util.List; | ||
|
||
/** Handles holding onto the cache headers for an entry. */ | ||
@VisibleForTesting |
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 annotation doesn't make sense here; this class needs to be package-visible in order for other classes in this package to use it.
We use this annotation when we increase the visibility beyond what it otherwise would be for the sole purpose of making it accessible in a unit test.
You should see Android Studio complaining about this, e.g. where DiskBasedAsyncCache is calling:
cb.onGetComplete(entry.toCacheEntry(data));
because it thinks toCacheEntry is only visible for testing, but here we're using it from prod code.
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
public void completed(Integer result, ByteBuffer attachment) { | ||
// if the file size changes, return null | ||
if (size != file.length()) { | ||
VolleyLog.d( |
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.
logging stuff:
- use e instead of d - this is an error, not debugging info
- You can do
VolleyLog.e("File changed while reading: %s", file.getAbsolutePath())
- no need to use format args for a constant string. Also, I think it's clearer to have the error be the first thing, and specific details like the file name come second. - Here and throughout, it should be "%s", not "s%", no?
if (success) { | ||
latch.countDown(); | ||
} else { | ||
return; |
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 need for "return" statement as the last statement in a void method, here and below.
AS highlights these in yellow for me - do you not see that too? In general, we should aim to have no warnings in new code.
return; | ||
} | ||
if (attachment.hasArray()) { | ||
final int offset = attachment.arrayOffset(); |
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 exact error message you get when you remove this line?
I think it's probably an ErrorProne thing which should probably be suppressed by adding a suppression for that, not by introducing a new variable that is itself unused and a compiler warning. arrayOffset() does nothing internally; we know there is a backing array because we allocated with ByteBuffer.allocate() above.
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 error is: "ByteBuffer.array() shouldn't be called unless ByteBuffer.arrayOffset() is used or if the ByteBuffer was initialized using ByteBuffer.wrap() or ByteBuffer.allocate().
byte[] data = attachment.array();
^
(see https://errorprone.info/bugpattern/ByteBufferBackingArray)"
I believe there is a way to just suppress the warning though. So I'll figure that out.
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.
One option would be to just pass Void/null for the attachment parameter and access buffer directly - that would prove to error prone that it's actually the same object which came from ByteBuffer.allocate. As it stands, ErrorProne has no way of knowing 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.
Yeah, I think that should work
|
||
@Override | ||
public void failed(Throwable exc, ByteBuffer attachment) { | ||
VolleyLog.d("%s: %s", file.getAbsolutePath(), exc.toString()); |
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.
read failures are abnormal - use e, and log the full stack trace:
VolleyLog.e(exc, "Failed to read file %s", file.getAbsolutePath());
} | ||
}); | ||
} catch (IOException e) { | ||
cb.onGetComplete(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.
better, but see above note regarding how to handle errors
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.
Changes:
- Remove AtomicReference
- Fix double callback
Consider replacing CountDownLatch with Future or CompletionStage.
@Override | ||
public final Entry get(String key) { | ||
final CountDownLatch latch = new CountDownLatch(1); | ||
final AtomicReference<Entry> entryRef = new AtomicReference<>(); |
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 use of AtomicReference
is unnecessary and should be removed. (In general, AtomicReference is rarely needed except in lock-free code.)
CountDownLatch
is also seldom needed because there are usually better choices in java.util.concurrent (see JCIP), esp. when the count is 1.
Looking at the intent of this synchronization, I think a Future
"value" would be a better match here, and it can be canceled.
CompletableFuture
is available in Android API 26. It provides a settable value that can be waited on. In fact, CompletableFuture's CompletionStage
may be a great interface for this enhancement, as it supports composition and chaining, and enables better exception handling.
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.
Ended up not having AsyncCache implement Cache at all, so there was no long a need for this.
final File file = getFileForKey(key); | ||
final int size = (int) file.length(); | ||
Path path = Paths.get(file.getPath()); | ||
AsynchronousFileChannel afc = 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.
I would move this inside the outer try, which should be a try-catch(IOException), then open a try-finally to close the afc on the next line:
try {
final AsynchronousFileChannel afc = open()
try {
// I/O
} finally {
afc.close()
}
} catch (IOException e) {
// handle e
}
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.
Since the class now requires a higher android api, I switched it to use a try-with-resources. I think this should allow it to just be a simple try-catch, and gets rid of these issues.
} | ||
}); | ||
} catch (IOException e) { | ||
cb.onGetComplete(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.
Line 106 is still potentially a second invocation of callback.
See comment above, and this example of another way to structure this:
I've addressed all the comments and concerns. I updated the try-try finally-catch block, into a try-with-resources, now that we are requiring that SDK. I also made AsyncCache not implement Cache anymore. |
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.
Just a few nits left.
For context - we decided to remove the blocking methods in AsyncCache because I don't think we'll need them. Since the calling code already needs to be able to support Cache or AsyncCache differently since we won't easily be able to provide an AsyncCache implementation pre-O (due to lack of nio classes in the platform), there isn't much benefit to having AsyncCache implement Cache; we're still going to want to keep a separate DiskBasedCache implementation as currently stands.
import androidx.annotation.Nullable; | ||
|
||
/** | ||
* AsyncCache is an abstract class which implements the Cache interface for backwards compatibility, |
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.
nit: update Javadoc now that we no longer implement Cache. My suggestion would just be:
/** Asynchronous equivalent to the {@link Cache} interface. */
As that is a simple summary of what this class is, and links to Cache in a more structured form so you can leverage the existing docs there if someone is looking for more detail.
public abstract class AsyncCache { | ||
|
||
public interface OnGetCompleteCallback { | ||
void onGetComplete(@Nullable Cache.Entry entry); |
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.
nit: Could you add a Javadoc for this method? It doesn't have to be super-detailed, but one thing I think would be good to include is when/why entry would be null, and what we do in the event of an error. Something like:
/**
* Invoked when the read from the cache is complete.
*
* @param entry The entry read from the cache, or null if the cache did not contain an entry for the given key or
* if the entry couldn't be read.
*/
public abstract void get(String key, OnGetCompleteCallback callback); | ||
|
||
public interface OnPutCompleteCallback { | ||
void onPutComplete(boolean success); |
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.
Cache#put returns void today, so the introduction of a success boolean is a new concept that I'm not sure we should introduce. Since the focus here is get, I might suggest leaving this out for now.
/** Returns the cache entry with the specified key if it exists, null otherwise. */ | ||
@Override | ||
public void get(String key, OnGetCompleteCallback callback) { | ||
final OnGetCompleteCallback cb = callback; |
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 think this line of code is only needed to mark this as final so the inner class can use it? If so, you can just mark the callback param as final (this doesn't require changes to AsyncCache) and use that directly.
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, I didn't realize I could do that
} | ||
}); | ||
} catch (IOException e) { | ||
VolleyLog.e("%s %s", file.getAbsolutePath(), e.toString()); |
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.
VolleyLog.e(e, "Failed to read file %s", file.getAbsolutePath())
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.
Sorry, one other thing I realized - we have some common code moved out / copied from DiskBasedCache now so we can share it in DiskBasedAsyncCache, but DiskBasedCache is unchanged in this PR. Shouldn't it also be updated to leverage the new common classes? Otherwise we still have two copies of the logic. Or is the plan to do that separately? (That's probably reasonable, though we should plan to do it soon).
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 would like to see the copied code unit tested before this is merged, which is probably easiest done by refactoring DiskBasedCache and its unit tests to leverage/test the code in its new location.
In particular, there should only be one implementation of the version-stamped cache header, and it should be thoroughly tested.
I also hate to see any code moved/copied from under test to out-from-under test, even for a short period of time.
These tests are already written and run automatically. Why delay?
…ctored to reflect this change.
DiskBasedCache and DiskBasedCache tests now uses the shared classes. I moved some stuff (like CountingInputStream) back from DiskBasedCacheUtility to DiskBasedCache, since the async version does not use it. The tests were also refactored and test the code in its new location. All comments should have been addressed now. |
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.
Looks good to me, just a few nits! Please update the PR title now that AsyncCache only has one get method.
|
||
public interface OnGetCompleteCallback { | ||
/** | ||
* Callback that's invoked when the read from the cache is complete. |
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.
Small nitpick, but prefer starting a method javadoc with a verb: Invoked when the read...
* Retrieves an entry from the cache and send it back through the callback function | ||
* | ||
* @param key Cache key | ||
* @param callback Callback function that sets the retrieved entry and lets the program know it |
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.
nit: Callback that will be notified when the information has been retrieved
is a bit more concise
@@ -374,14 +369,21 @@ private void removeEntry(String key) { | |||
} | |||
} | |||
|
|||
InputStream createInputStream(File file) throws FileNotFoundException { |
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 these no longer @VisibleForTesting
?
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 had moved them outside of DiskBasedCache, but now since they're there again, I'll add it back
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.
nit: it would lower the diff output if you moved them back to the same location they were in before, which helps in reviewing
/** Handles holding onto the cache headers for an entry. */ | ||
class CacheHeader { | ||
/** Magic number for current version of cache file format. */ | ||
private static final int CACHE_MAGIC = 0x20150306; |
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.
Could you use the CACHE_MAGIC
from DiskBasedCacheUtility
instead? This way, we avoid duplicate code and potential mismatches in the future if one is modified but not the other.
@VisibleForTesting static final float HYSTERESIS_FACTOR = 0.9f; | ||
|
||
/** Magic number for current version of cache file format. */ | ||
static final int CACHE_MAGIC = 0x20150306; |
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.
Note that I don't see this being used outside of CacheHeader, so you could consider removing it from here and keep the one in CacheHeader
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.
Yeah, that's smart. I'll make that change.
} | ||
|
||
/** Returns a file object for the given cache key. */ | ||
public File getFileForKey(String key) { |
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 this be private?
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, fixed
Thanks! |
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.
Thanks for preserving the tests! I don't have any more changes to request. If you find an outstanding request, please decline it.
@@ -374,14 +369,21 @@ private void removeEntry(String key) { | |||
} | |||
} | |||
|
|||
InputStream createInputStream(File file) throws FileNotFoundException { |
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.
nit: it would lower the diff output if you moved them back to the same location they were in before, which helps in reviewing
@@ -501,86 +503,87 @@ public void testCountingInputStreamByteCount() throws IOException { | |||
public void testEmptyReadThrowsEOF() throws IOException { | |||
ByteArrayInputStream empty = new ByteArrayInputStream(new byte[] {}); | |||
exception.expect(EOFException.class); | |||
DiskBasedCache.readInt(empty); | |||
DiskBasedCacheUtility.readInt(empty); |
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.
For tests which solely exercise methods in DiskBasedCacheUtility, we should move their tests to a new DiskBasedCacheUtilityTest rather than having them in here, since they aren't specific to DiskBasedCache any more.
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 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.
Scott, did you upload a new DiskBasedCacheUtilityTest class for the tests you removed from here? I don't see one currently
|
||
/** | ||
* Retrieves an entry from the cache and send it back through the callback function | ||
* |
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.
nit: "sends it back"
Also would be good to link to the callback – {@link OnGetCompleteCallback#onGetComplete}
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.
changed this. I realized I never added the DiskBasedCacheUtilityTest class to the commit, since it was untracked. Should have just fixed that as well.
@@ -501,86 +503,87 @@ public void testCountingInputStreamByteCount() throws IOException { | |||
public void testEmptyReadThrowsEOF() throws IOException { | |||
ByteArrayInputStream empty = new ByteArrayInputStream(new byte[] {}); | |||
exception.expect(EOFException.class); | |||
DiskBasedCache.readInt(empty); | |||
DiskBasedCacheUtility.readInt(empty); |
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.
Scott, did you upload a new DiskBasedCacheUtilityTest class for the tests you removed from here? I don't see one currently
Uploaded the new test class and fixed the javadocs mistakes. |
By the way, there are 100+ warnings in the Travis build mostly/all about annotations: https://travis-ci.org/github/google/volley/builds/701650084 I wish the Travis build (or GitHub Action?) reported code coverage. |
Filed #345 for the warnings - these are unrelated to this PR and not a major problem since they only impact the Javadoc (if at all). Coverage is a nice to have, but I don't see it as super critical since we can watch for it in PRs; the project is not that big and doesn't have many people approving changes. |
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 wait for @jbir789 to sign off in case he has further comments before merging, but LGTM. Thanks!
Implemented the get function in AsyncCache that takes in a callback function in two different ways. In the one (DiskBasedAsyncCache), it uses an asynchronous file channel, and for the fallback version, it works very similarly to the current DiskBasedCache. Most of the code in those files are from that, aside from the get method. There are not tests yet for this, as not enough has really been implemented to test it, but was more to check the direction of the code.