Skip to content

RUM-3889 Send retry information into rum data upload requests#2298

Merged
mariusc83 merged 1 commit into
developfrom
mconstantin/rum-3889/send-retry-information-into-request
Oct 3, 2024
Merged

RUM-3889 Send retry information into rum data upload requests#2298
mariusc83 merged 1 commit into
developfrom
mconstantin/rum-3889/send-retry-information-into-request

Conversation

@mariusc83

Copy link
Copy Markdown
Member

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)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

@mariusc83 mariusc83 self-assigned this Sep 30, 2024
@mariusc83
mariusc83 force-pushed the mconstantin/rum-3889/send-retry-information-into-request branch from 22f9794 to 8aa7a2d Compare September 30, 2024 10:17
@codecov-commenter

codecov-commenter commented Sep 30, 2024

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 74.31193% with 28 lines in your changes missing coverage. Please review.

Project coverage is 70.27%. Comparing base (21025fd) to head (d0a1efa).
Report is 16 commits behind head on develop.

Files with missing lines Patch % Lines
...adog/android/rum/internal/net/RumRequestFactory.kt 48.00% 25 Missing and 1 partial ⚠️
...id/core/internal/data/upload/DataOkHttpUploader.kt 86.67% 0 Missing and 2 partials ⚠️
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     
Files with missing lines Coverage Δ
...datadog/android/api/net/RequestExecutionContext.kt 100.00% <100.00%> (ø)
...tlin/com/datadog/android/api/net/RequestFactory.kt 100.00% <ø> (ø)
...id/core/internal/data/upload/DataUploadRunnable.kt 96.49% <100.00%> (ø)
.../android/core/internal/data/upload/DataUploader.kt 100.00% <100.00%> (ø)
...roid/core/internal/data/upload/NoOpDataUploader.kt 50.00% <ø> (ø)
.../android/core/internal/data/upload/UploadStatus.kt 98.81% <100.00%> (+0.13%) ⬆️
.../android/core/internal/data/upload/UploadWorker.kt 84.62% <100.00%> (+0.40%) ⬆️
...dog/android/log/internal/net/LogsRequestFactory.kt 100.00% <ø> (ø)
...sionreplay/internal/net/ResourcesRequestFactory.kt 71.43% <ø> (ø)
...essionreplay/internal/net/SegmentRequestFactory.kt 100.00% <ø> (ø)
... and 3 more

... and 29 files with indirect coverage changes

@mariusc83
mariusc83 force-pushed the mconstantin/rum-3889/send-retry-information-into-request branch 2 times, most recently from 600c0cb to d8a88da Compare September 30, 2024 12:03
@mariusc83
mariusc83 marked this pull request as ready for review September 30, 2024 12:38
@mariusc83
mariusc83 requested review from a team as code owners September 30, 2024 12:38
if (idempotencyKey != null) {
headers[RequestFactory.DD_IDEMPOTENCY_KEY] = idempotencyKey
}
return headers

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

maybe headers.toMap in the end to make it read-only again?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

@mariusc83 mariusc83 Sep 30, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this @Supress necessary? it seems you catch all the possible exceptions

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

good questions...it was before I added this in the detekt.xml let me see it now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

https://detekt.dev/docs/rules/exceptions/#toogenericexceptioncaught

/**
* 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I am going to try to be more explicit here.

Comment on lines +15 to +21
/**
* 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.
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* @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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

will it be used for any deduplication actually? I was under the impression that this is mostly for collecting the statistics right now

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Well yes maybe not for now, not sure should we change this definition ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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...

Comment on lines +131 to +136
private fun idempotencyKey(byteArray: ByteArray): String? {
try {
val digest = MessageDigest.getInstance("SHA-1")
val hashBytes = digest.digest(byteArray)
return hashBytes.toHexString()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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 ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 ?

@mariusc83 mariusc83 Oct 1, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

actually no - scratch above, we don't keep in memory. the deletion of batch for other than successful upload can be a problem here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

@mariusc83
mariusc83 force-pushed the mconstantin/rum-3889/send-retry-information-into-request branch 3 times, most recently from 6253373 to e113d66 Compare October 2, 2024 08:29
Comment on lines +136 to +138
if (isNotEmpty()) {
append(" ")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here's a proposal to make this property more understandable at a glance.

Suggested change
val attempts: Int = 0,
val attemptNumber: Int = 0,

Comment on lines +110 to +112
UploadStatus.RETRY_REQUEST_LOG_MESSAGE_FORMAT.format(
Locale.US, fakeRequestAttempts, status.code
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

You should not do that as you're using the same algorithm to create the expected output as the production code.

@mariusc83 mariusc83 Oct 2, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yup I realized that, I am fixing it now, I am afraid I will have to do this in function of the target.

@mariusc83
mariusc83 force-pushed the mconstantin/rum-3889/send-retry-information-into-request branch 2 times, most recently from 1059e44 to 40216ec Compare October 2, 2024 12:53
is UnknownHttpError -> listOf(InternalLogger.Target.USER)
}

private fun resolveLevel() = when (this) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

Naming nit picking to make things clearer

Suggested change
private fun resolveLevel() = when (this) {
private fun resolveInternalLogLevel() = when (this) {

)
}

private fun resolveTarget() = when (this) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

Naming nit picking to make things clearer

Suggested change
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here's a proposal to make this property more understandable at a glance.

Suggested change
val attempts: Int = 0,
val attemptNumber: Int = 0,

Comment on lines +12 to +14
* @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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @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.

@mariusc83
mariusc83 force-pushed the mconstantin/rum-3889/send-retry-information-into-request branch from 40216ec to 930e689 Compare October 2, 2024 14:40
@mariusc83
mariusc83 requested a review from xgouchet October 2, 2024 15:20
	modified:   dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/data/upload/DataOkHttpUploader.kt
@mariusc83
mariusc83 force-pushed the mconstantin/rum-3889/send-retry-information-into-request branch from 930e689 to d0a1efa Compare October 3, 2024 09:04
@mariusc83
mariusc83 merged commit 7ebe827 into develop Oct 3, 2024
@mariusc83
mariusc83 deleted the mconstantin/rum-3889/send-retry-information-into-request branch October 3, 2024 11:21
Comment on lines +34 to +36
private var attempts = 1
private var previousUploadStatus: UploadStatus? = null
private var previousUploadedBatchId: BatchId? = null

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This potentially may be not a thread-safe, leading to the wrong data read. The thing is that here

uploadExecutorService = LoggingScheduledThreadPoolExecutor(
we create upload executor with core pool size set, but it doesn't mean I believe, that it will be always only 1 thread there. It means only that 1 thread will be kept alive when there is idle.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants