RUM-3889 Send retry information into rum data upload requests#2298
Conversation
22f9794 to
8aa7a2d
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #2298 +/- ##
===========================================
- Coverage 70.44% 70.27% -0.17%
===========================================
Files 730 732 +2
Lines 27268 27332 +64
Branches 4584 4587 +3
===========================================
- Hits 19207 19205 -2
- Misses 6815 6856 +41
- Partials 1246 1271 +25
|
600c0cb to
d8a88da
Compare
| if (idempotencyKey != null) { | ||
| headers[RequestFactory.DD_IDEMPOTENCY_KEY] = idempotencyKey | ||
| } | ||
| return headers |
There was a problem hiding this comment.
maybe headers.toMap in the end to make it read-only again?
There was a problem hiding this comment.
yup I also had this in mind but I was thinking that might be a bit overkill...I could actually do it differently without having 2 create 2 maps. Let me change this.
There was a problem hiding this comment.
Actually the reason why I did this was that I return a immutable map. If you call toMap under the hood it also creates an LinkedHashMap so still mutable.
There was a problem hiding this comment.
So basically I will leave it like this because under the hood after a quick research toMap returns like I mentioned above a LinkedHashMap. Actually I would have expected to return an ImmutableMap but apparently the immutability is only at the type (surface level). So in a Kotlin code you could easily do this:
val map = mapOf(...)
val immutableMap = map.toMap()
val hashMap = immutableMap as LinkedHashMap
| return elements.joinToString(",") | ||
| } | ||
|
|
||
| @Suppress("TooGenericExceptionCaught") |
There was a problem hiding this comment.
is this @Supress necessary? it seems you catch all the possible exceptions
There was a problem hiding this comment.
good questions...it was before I added this in the detekt.xml let me see it now.
There was a problem hiding this comment.
yes it is still complaining and due to the NullPointerException catch at the end of the block. I am not sure why though as this is specifically thrown by the code under the hood.
There was a problem hiding this comment.
It's a default set of detekt to include NPE in this check, because for Kotlin code, NPE is not very informative, but since we are calling a Java function, there is nothing we can do about, so it's correct to have @Suppress here I think.
| /** | ||
| * Provides information about the current request execution context. | ||
| * @param attempts the number of attempts made to execute the request in case of a retry. | ||
| * @param previousResponseCode the response code of the previous request failure code in case of a retry. |
There was a problem hiding this comment.
Seems like this sentence should be corrected. It should add at least the comma and state, that this is a code of the previous attempt, otherwise one may think that it can be the code of the previous batch upload.
There was a problem hiding this comment.
I am going to try to be more explicit here.
| /** | ||
| * The number of attempts made to execute the request in case of a retry. | ||
| */ | ||
| val attempts: Int = 0, | ||
| /** | ||
| * The response code of the previous request failure code in case of a retry. | ||
| */ |
There was a problem hiding this comment.
why duplicate the description there if it is already in the Kdoc above? These ones won't be treated as documentation and are ordinary comments.
| /** | ||
| * Creates a request for the given batch. | ||
| * @param context Datadog SDK context. | ||
| * @param executionContext Gives information about the execution context of this and previous requests. |
There was a problem hiding this comment.
| * @param executionContext Gives information about the execution context of this and previous requests. | |
| * @param executionContext Information about the execution context of this and previous requests. |
Also to avoid confusion we must say that it is for the current batch only (otherwise it is not clear if it is for the previous batch as well).
| const val QUERY_PARAM_TAGS: String = "ddtags" | ||
|
|
||
| /** | ||
| * Datadog Idempotency key header, used for deduplication purposes. |
There was a problem hiding this comment.
will it be used for any deduplication actually? I was under the impression that this is mostly for collecting the statistics right now
There was a problem hiding this comment.
Well yes maybe not for now, not sure should we change this definition ?
There was a problem hiding this comment.
I'm not even sure it will ever will. And anyway, if it will be, it will be used only for RUM events.
| ) | ||
| } | ||
| if ([email protected]) { | ||
| append( |
There was a problem hiding this comment.
should add a space in the beginning in case if there is already some text, otherwise it will be like ...targeting the relevant Datadog site.Attempt count was...
| private fun idempotencyKey(byteArray: ByteArray): String? { | ||
| try { | ||
| val digest = MessageDigest.getInstance("SHA-1") | ||
| val hashBytes = digest.digest(byteArray) | ||
| return hashBytes.toHexString() |
There was a problem hiding this comment.
So each feature has to provide the key on its own? I'm not sure if it is the solution we would like to have. Is it possible to lift it to the core? Or at least have something like ByteArray.sha1() declared in the core. How iOS SDK is doing that?
There was a problem hiding this comment.
I went to the core way but then I've seen that in iOS implementation they did it at the feature level and changed it. I think it makes sense at the feature level as Logs and Traces will not need this. Otherwise in the core you will have to make sure you do it only for the right features (if/else) and not even sure if we have the feature information in that place to check.
There was a problem hiding this comment.
They have a function to generate the key in the core https://github.com/DataDog/dd-sdk-ios/blob/dec751b901e17e74bc7f96af56558882cacd9282/DatadogInternal/Sources/Extensions/Data%2BCrypto.swift#L11 and it is applied on the each feature level. We should follow the same approach.
There was a problem hiding this comment.
The only problem I have with this is that for now we are doing this only for RUM and I am afraid it will stay like that. I have no problem with moving this into core but that's the reason I preferred not to do it. Should we do it now or later in case we really need to share it between features ?
There was a problem hiding this comment.
I mean for now we can just declare extension method to generate SHA1 in the core and call it from the feature-specific factory. We already have the header declared in the core, isn't it?
There was a problem hiding this comment.
I get the part with the function to have the extension in the core but don't get the header part ? The header is added here so you only need ByteArray.generateSha1Fingerprint() function if we want to go this way. I am still thinking it is overkilled as no other code will ever use this function but will let it here for others to say their opinion. @ambushwork @xgouchet @jonathanmos ?
| val code: Int = UNKNOWN_RESPONSE_CODE, | ||
| val throwable: Throwable? = null | ||
| val throwable: Throwable? = null, | ||
| val attempt: Int = 0 |
There was a problem hiding this comment.
Warning
Should we really add the attempt information here ? The attempt is not an information of a single upload status, but more a reflection of several attempts, by definition. I feel like it doesn't really have its place in here. The question makes even more sense since some attempt failures don't set this attempt value.
Also the name is a big ambiguous, is it the number of attempts before this one ? Or is it the count of attempts including this one ?
There was a problem hiding this comment.
While I agree this could be done in different ways the initial plan for me was to align to iOS implementation where this was added to the UploadStatus information, I also kept their naming -> attempt.
| batch: List<RawBatchEvent>, | ||
| batchMeta: ByteArray? | ||
| batchMeta: ByteArray?, | ||
| previousUploadStatus: UploadStatus? |
There was a problem hiding this comment.
Warning
It feels like we're making a whole lot of change in unrelated classes to simply get the number of attempts for a given batch.
If in the end we just keep the info in a previousUploadStatus property, why don't we put that info in the DataOkHttpUploader class directly, instead of the DataUploadRunnable?
Important
Also there's a key thing here that we don't track is which batch was the previous upload status related to. Technically, we could make 13 attempts with batch A. Then batch A is deleted for being too old, and we make one successful attempt with batch B, which would report 13 retries… Is this the behavior we want to track ?
There was a problem hiding this comment.
yup here is a corner case and having followed the exact implementation in iOS I think they have the same corner case. I am going to open the discussion in the daily today to make sure everyone is aware.
There was a problem hiding this comment.
We have 2 options here, either keep the information in the UploadStatus and handle the previousUpload status by its batchID or move everything into the DataOkHttpUploader but I would rather not do it there as we do not have any information about the batch id and we will have to compute the fingerprint from the batchData or pass the batchID as a extra parameter to that method.
There was a problem hiding this comment.
the answer to this corner this depends on implementation on when you are reading the content from the file, for iOS it is not a problem because the batch is already in the memory and then retry state management is done. If you delete the files, it doesn't matter, we have the data in memory already read in case of iOS - this is by design and I would even say it is bad design and can cause other issues but not with retry information.
There was a problem hiding this comment.
actually no - scratch above, we don't keep in memory. the deletion of batch for other than successful upload can be a problem here.
There was a problem hiding this comment.
I would suggest save the batch identifier in the upload status just like retry information and only propagate the information when the identifier is same - else reset it.
I don't see any reason to keep this per batch, only for the active batch is sufficient.
There was a problem hiding this comment.
I agree with this solution, this will mean that we can keep same implementation and just pass the batchID in the previous UploadStatus to match it here.
6253373 to
e113d66
Compare
| if (isNotEmpty()) { | ||
| append(" ") | ||
| } |
There was a problem hiding this comment.
Note
The builder will never be empty at this point, the space can be included in the RETRY_REQUEST_LOG_MESSAGE_FORMAT
| "and you're targeting the relevant Datadog site." | ||
| ) | ||
| } | ||
| if (code != null && attempt != null) { |
There was a problem hiding this comment.
Note
What if there was several attempts but the last status doesn't have a status code (e.g.: network failure)? I think only the attempt should be checked. Besides, the beginning of the message already gives information about the status and code, so it feels redundand to add it here.
| * @param previousResponseCode the response code of the previous request failure code in case of a retry. | ||
| */ | ||
| data class RequestExecutionContext( | ||
| val attempts: Int = 0, |
There was a problem hiding this comment.
Warning
Is this the number of attempts including the current one being made, or excluding it? It would be better to make this info explicit in the doc and in the name.
There was a problem hiding this comment.
Yes I agree, I need to add more info here as this is quite confusing. Basically to answer to your question, it should contain also the current one to be consistent but in that case the previousStatusCode will be null. In the RequestFactory we will not send this information if the previousStatusCode is null
There was a problem hiding this comment.
Here's a proposal to make this property more understandable at a glance.
| val attempts: Int = 0, | |
| val attemptNumber: Int = 0, |
| UploadStatus.RETRY_REQUEST_LOG_MESSAGE_FORMAT.format( | ||
| Locale.US, fakeRequestAttempts, status.code | ||
| ) |
There was a problem hiding this comment.
Warning
You should not do that as you're using the same algorithm to create the expected output as the production code.
There was a problem hiding this comment.
yup I realized that, I am fixing it now, I am afraid I will have to do this in function of the target.
1059e44 to
40216ec
Compare
| is UnknownHttpError -> listOf(InternalLogger.Target.USER) | ||
| } | ||
|
|
||
| private fun resolveLevel() = when (this) { |
There was a problem hiding this comment.
Note
Naming nit picking to make things clearer
| private fun resolveLevel() = when (this) { | |
| private fun resolveInternalLogLevel() = when (this) { |
| ) | ||
| } | ||
|
|
||
| private fun resolveTarget() = when (this) { |
There was a problem hiding this comment.
Note
Naming nit picking to make things clearer
| private fun resolveTarget() = when (this) { | |
| private fun resolveInternalLogTarget() = when (this) { |
| * @param previousResponseCode the response code of the previous request failure code in case of a retry. | ||
| */ | ||
| data class RequestExecutionContext( | ||
| val attempts: Int = 0, |
There was a problem hiding this comment.
Here's a proposal to make this property more understandable at a glance.
| val attempts: Int = 0, | |
| val attemptNumber: Int = 0, |
| * @param attempts the number of attempts made to execute this particular request. We take into account the batch ID | ||
| * and not the request ID (which is unique for each request) when computing the number of attempts. | ||
| * This takes into account the initial request and all the retries. |
There was a problem hiding this comment.
| * @param attempts the number of attempts made to execute this particular request. We take into account the batch ID | |
| * and not the request ID (which is unique for each request) when computing the number of attempts. | |
| * This takes into account the initial request and all the retries. | |
| * @param attemptNumber the number of this attempt for a specific batch. It'll be 1 for the first attempt, and will be incremented each time an upload for the same batch is retried. |
40216ec to
930e689
Compare
modified: dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataOkHttpUploader.kt
930e689 to
d0a1efa
Compare
| private var attempts = 1 | ||
| private var previousUploadStatus: UploadStatus? = null | ||
| private var previousUploadedBatchId: BatchId? = null |
There was a problem hiding this comment.
This potentially may be not a thread-safe, leading to the wrong data read. The thing is that here
Now we share this executor with particular features which submit runnables to this executor and while for the feature all its runnables will be executed in the sequential way, there is a low chance that under the high load it may be more than 1 thread in this executor at runnable N can be executed by other thread than runnable N-1.
I may be wrong here, but on the quick look it is a concern.
There was a problem hiding this comment.
I particularly looked into this myself also. Initially my intention was to make them thread safe but then I didn't want to add this if it's not really needed. I checked the executors and seen the 1 pool thread size and assumed it is fine. I am thinking that if we really have this scenario that you are mentioning we might end up in different problems as the upload runnable assumes that everything is executed sequentially.
There was a problem hiding this comment.
if we really have this scenario that you are mentioning we might end up in different problems as the upload runnable assumes that everything is executed sequentially
And it will be executed sequentially (in the boundaries of the particular feature) even if there are multiple threads in the pool, because there is only one single runnable at the time for the particular feature and the next runnable is scheduled only at the end of the execution of the previous feature. And underlying storage is thread-safe, it is designed for the concurrent usage.
Here my point is only about volatile nature of these references. Imagine there is an executor running and thread was killed because of some reason; then new thread will be spinned off which may not see the changes made to these references by the previous thread. Or in the case if executor has multiple threads running.
There was a problem hiding this comment.
That might be a problem indeed. I think marking them as volatile might make sense to make sure they are being kept in the HEAP and not in the Thread local memory. In case of atomicity we don't need it in this case as we are already running sequential. I am going to open a PR.
What does this PR do?
Following the internal RFC specifications we are adding new information for the RUM upload requests (retry count, previous retry failure code and request SHA1 idempotency key) in order to help understanding the performances of or our retry mechanism.
Motivation
What inspired you to submit this pull request?
Additional Notes
Anything else we should know when reviewing?
Review checklist (to be filled by reviewers)