You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Cloud spanner client does not seem to be retrying broken streams #3775
If streaming a chunk fails due to a “resumable” error, Session.read() retries the StreamingRead API reqeust, passing the resume_token from the last partial result streamed.
This needs to be done at a higher level than GAX, since we send resume tokens along with the responses. So if a stream breaks, instead of retrying the whole stream, the library should just pass the resume token back in the request which will cause the stream to be resumed from that point onwards. It is further complicated since cloud spanner might not send resume tokens with all responses so the client library needs to buffer responses till it sees one with resume token and only then yield them to the user. This will allow it to safely resume in case the stream breaks.
This is the Java implementation of the same logic: https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java#L2166
What does GAX do for streaming calls? Does it retry the whole stream if it fails? What if the stream had yielded all but the last 1 response before failing? In that case retrying the whole stream is very wasteful. If GAX does do retries for streaming calls, we need to disable them.
What does GAX do for streaming calls? Does it retry the whole stream if it fails? What if the stream had yielded all but the last 1 response before failing? In that case retrying the whole stream is very wasteful. If GAX does do retries for streaming calls, we need to disable them.
gax is pretty naive in what it does for anything other than simple RPCs. I'm writing a doc internally on this now. :)
Till this is resolved can we fix the documentation to indicate that users would need to resume the stream themselves by passing back the resume token if available?
Snapshot.read and Snapshot.execute_sql return an instance of StreamedResultSet. That instance does not contain the information needed to reconstruct the original call inside its consume_next, which means that we cannot make handling such errors seamless to the user out-of-the box.
We could attempt to add enough information to the StreamedResultSet (a curried partial, maybe?) to allow such retries. E.g., from read():
The only other way I can see to hide such errors from the user would be to add callback-based methods which took the arguments to be passed to read / execute_sql, executed them, and then iterated the result set, invoking the callback for each row successfully pulled from the stream.
I don't understand how the callback solution fixes this. Can you please elaborate? How would you retry in that case?
Your option 1 is similar to what we do in Java as well. Note that when you retry you will have to pass back the resume_token, so the retry function could possible take a resume token as an argument.
Also note this is more complex because we do not guarantee that all PartialResultSet would have a resume token. So if you yield a Row to the user but the PRS that contained that Row did not have a resume_token, you cannot retry safely. So what you will need to do is store PRS's in a buffer till you see one with resume_token. Only then yield them to the user. So essentially you guarantee that every time you yield something to the user it is at a resumable boundary.
The curried partial is the pattern we'll use in google.api.core. Instead of passing the curried partial to StreamedResultSet, can you construct an intermediary iterator wrapper for the request?
I applied the release blocking label temporarily, to make sure we reach some kind of resolution on this before Beta. Either there's a quick and easy fix, or we update the docs so that we're not misleading users. In the latter case, we'll want to fix this shortly after declaring Beta.
@jonparrott the "stock" bits in google.api.core.retry don't really fit this case: I need to inject a "fixup" when the appropriate exception (UNAVAILABLE) is raised, before retrying the stream iteration. Something like:
defconsume_next(self):
"""Consume the next partial result set from the stream. Parse the result set into new/existing rows in :attr:`_rows` """sleep_generator=retry.exponential_sleep_generator(
initial=retry._DEFAULT_INITIAL_DELAY,
maximum=retry._DEFAULT_MAXIMUM_DELAY)
forsleepinsleep_generator:
try:
response=six.next(self._response_iterator)
exceptexceptions.ServiceUnavailable:
ifself._resume_tokenin (None, b''):
raiseself._response_iterator=self._retry(
resume_token=self._resume_token)
else:
break
And of course I have to add my own deadline handling.
BTW, why does google.api.core.retry.retry_target use the more convoluted datetime stuff to compute the deadline, rather than just time.time plus a constant number of seconds?
BTW, why does google.api.core.retry.retry_target use the more convoluted datetime stuff to compute the deadline, rather than just time.time plus a constant number of seconds?
No particular reason other than being functionally equivalent and having on single source of truth for time in tests (datetime_helpers.utcnow()).
And of course I have to add my own deadline handling.
And of course I have to add my own deadline handling.
How so?
As you pointed out above, I'm doing the low level work myself.
ISTM that the retry_target (and therefore Retry) could grow some feature to signal a "fixup" to happen between the retries, to accommodate this kind of stateful behavior.
It wouldn't let you catch the error and replace self._response_iterator.
I could imagine a hack using the predicate and some getattr magic, but that sounds gross to me.
Is there a minimal change you could make to retry_target to make it work for you- perhaps an optional on_exception callback? If so, I'm happy to review and merge that.
Activity
vkedia commentedon Aug 9, 2017
cc @bjwatson @tseaver @lukesneeringer
lukesneeringer commentedon Aug 9, 2017
Our retry logic is in GAX, but leaving this open for @tseaver to confirm that it is applicable here.
vkedia commentedon Aug 9, 2017
This needs to be done at a higher level than GAX, since we send resume tokens along with the responses. So if a stream breaks, instead of retrying the whole stream, the library should just pass the resume token back in the request which will cause the stream to be resumed from that point onwards. It is further complicated since cloud spanner might not send resume tokens with all responses so the client library needs to buffer responses till it sees one with resume token and only then yield them to the user. This will allow it to safely resume in case the stream breaks.
This is the Java implementation of the same logic:
https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java#L2166
What does GAX do for streaming calls? Does it retry the whole stream if it fails? What if the stream had yielded all but the last 1 response before failing? In that case retrying the whole stream is very wasteful. If GAX does do retries for streaming calls, we need to disable them.
theacodes commentedon Aug 9, 2017
gax is pretty naive in what it does for anything other than simple RPCs. I'm writing a doc internally on this now. :)
bjwatson commentedon Aug 9, 2017
@lukesneeringer @vkedia Does this need to be Beta blocking?
vkedia commentedon Aug 10, 2017
Till this is resolved can we fix the documentation to indicate that users would need to resume the stream themselves by passing back the resume token if available?
tseaver commentedon Aug 10, 2017
Snapshot.readandSnapshot.execute_sqlreturn an instance ofStreamedResultSet. That instance does not contain the information needed to reconstruct the original call inside itsconsume_next, which means that we cannot make handling such errors seamless to the user out-of-the box.We could attempt to add enough information to the
StreamedResultSet(a curried partial, maybe?) to allow such retries. E.g., fromread():or from
execute_sql:The only other way I can see to hide such errors from the user would be to add callback-based methods which took the arguments to be passed to
read/execute_sql, executed them, and then iterated the result set, invoking the callback for each row successfully pulled from the stream.E.g.:
vkedia commentedon Aug 10, 2017
I don't understand how the callback solution fixes this. Can you please elaborate? How would you retry in that case?
Your option 1 is similar to what we do in Java as well. Note that when you retry you will have to pass back the resume_token, so the retry function could possible take a resume token as an argument.
Also note this is more complex because we do not guarantee that all PartialResultSet would have a resume token. So if you yield a Row to the user but the PRS that contained that Row did not have a resume_token, you cannot retry safely. So what you will need to do is store PRS's in a buffer till you see one with resume_token. Only then yield them to the user. So essentially you guarantee that every time you yield something to the user it is at a resumable boundary.
theacodes commentedon Aug 10, 2017
The curried partial is the pattern we'll use in
google.api.core. Instead of passing the curried partial toStreamedResultSet, can you construct an intermediary iterator wrapper for the request?bjwatson commentedon Aug 11, 2017
I applied the
release blockinglabel temporarily, to make sure we reach some kind of resolution on this before Beta. Either there's a quick and easy fix, or we update the docs so that we're not misleading users. In the latter case, we'll want to fix this shortly after declaring Beta.Document retry of exceptions while streaming 'read'/'execute_sql' res…
23 remaining items
tseaver commentedon Aug 28, 2017
@jonparrott the "stock" bits in
google.api.core.retrydon't really fit this case: I need to inject a "fixup" when the appropriate exception (UNAVAILABLE) is raised, before retrying the stream iteration. Something like:theacodes commentedon Aug 28, 2017
@tseaver that seems pretty reasonable. (it's one of the reasons I made exponential_sleep_generator public)
You should set a deadline of some sort, though.
vkedia commentedon Aug 28, 2017
tseaver commentedon Aug 28, 2017
And of course I have to add my own deadline handling.
BTW, why does
google.api.core.retry.retry_targetuse the more convoluteddatetimestuff to compute the deadline, rather than justtime.timeplus a constant number of seconds?theacodes commentedon Aug 28, 2017
No particular reason other than being functionally equivalent and having on single source of truth for time in tests (
datetime_helpers.utcnow()).How so?
tseaver commentedon Aug 28, 2017
@jonparrott:
As you pointed out above, I'm doing the low level work myself.
ISTM that the
retry_target(and thereforeRetry) could grow some feature to signal a "fixup" to happen between the retries, to accommodate this kind of stateful behavior.theacodes commentedon Aug 28, 2017
I'm still not sure what you mean? Just pass the user's deadline into retry_target?
tseaver commentedon Aug 28, 2017
I can't use
retry_target: it doesn't let me issue the "restart" request and capture the new iterator. And so I need to do my own deadline handling.theacodes commentedon Aug 28, 2017
Thanks for the PR - I think I see now, so it seems like if you did this:
It wouldn't let you catch the error and replace
self._response_iterator.I could imagine a hack using the predicate and some getattr magic, but that sounds gross to me.
Is there a minimal change you could make to
retry_targetto make it work for you- perhaps an optionalon_exceptioncallback? If so, I'm happy to review and merge that.theacodes commentedon Aug 28, 2017
Also - I've thought about a
retry_iteratorfunction - maybe it makes more sense to just go ahead and add that to core?Use '_restart_on_unavailable' wrapper in 'SRS.{read,execute_sql}.
Retry streaming exceptions (this time for sure, Rocky!) (#4016)
bjwatson commentedon Sep 22, 2017
Woo hoo! Thanks @tseaver!
Retry streaming exceptions (this time for sure, Rocky!) (#4016)