Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: add the response details to the offline request queue #393

Open
devj3ns opened this issue Jul 15, 2024 · 7 comments · May be fixed by #447
Open

Proposal: add the response details to the offline request queue #393

devj3ns opened this issue Jul 15, 2024 · 7 comments · May be fixed by #447

Comments

@devj3ns
Copy link
Contributor

devj3ns commented Jul 15, 2024

Context

I am currently adding a conflict detection system via triggers to my Postgres database, which rejects an update operation when a client who was offline before tries to sync its changes using the offline request queue, but the object was already changed meanwhile. This prevents the loss of data when synchronizing. In the above case, I would like the user to give the opportunity to decide whether to keep the local or remote changes. (In my app, it is not possible to automatically discard the local change in the case it is rejected by the remote database because this would lead to data loss).

Current situation

In cases where a request inside the offline queue is rejected by the remote provider, the offline queue gets blocked until the request is removed.

I already created a page in my app which displays all requests which are inside the offline queue so that the user can see the sync status. Now I want to give the user the option to delete requests that got rejected by the remote database because of a conflict. This would unblock the request queue and other requests could be sent to the remote provider.

Proposal

What is missing from brick to do this is the ability to see the response details. Currently, brick only increments the “attempts” number inside the offline queue when a request fails. Therefore, I would like to propose a change that the HTTP-Status-Code and body of the latest attempt is stored inside the offline queue database table as well. This way, the app could identify which request got rejected and give the user the ability to remove the request, which frees up the queue.

I am willing to create a Pull Request which extends brick to support this but wanted to get some feedback on this idea first. Especially @tshedor, what do you think of this?

@tshedor
Copy link
Collaborator

tshedor commented Jul 17, 2024

Brick intentionally doesn't support the serialization of the server response because of the extremely varied case in associating a request to a response. For example, say you're upserting 5 Pizza requests from a single customer. In your own app, you could associate 5 Pizza responses based on the customerId from each request to each response. This might work when the implementation is aware of the domain, but Brick is agnostic.

However, even if the implementation is aware of the domain, Brick needs to assume it could receives responses out of order from when the request was made (in case the requests are transmitted online, offline, online, offline, offline). So the question becomes, "How do you associate a request to a server response without controlling the server?" If you could control the server, something like an idempotency key could be used, but Brick can't guarantee the server.

The last option you could use is making the queue somewhat more stateful and storing the response immediately to a separate table using the local table's request id as a foreign key. However, in this solution, do you store the response for every request? Just the latest request? How much information would the end user need to determine if the request should be voided? And if you store these raw responses, do you need to worry about any data sanitization since the queue isn't encrypted by default (the model logic may transform strip/encrypt sensitive information)? Will disk space size become a factor? Will performance of the queue's primary function be affected?

So, overall, it's doable, but I would strongly consider the end user's interface (how they access this in their own app code) first - what's needed and how can it be accessed efficiently - and then work back to the solution. I agree that the OfflineRequestQueue class is the right place to start. I would recommend subclassing it - i.e. OfflineRequestQueueWithResponses or similar - because I'm not convinced this is necessary behavior for the average Brick user.

@tshedor
Copy link
Collaborator

tshedor commented Jul 17, 2024

the offline queue gets blocked until the request is removed.

Re this concern: in the RestRequestSqliteCacheManager there's a flag for serialProcessing. It's true by default. But if your app doesn't require requests to be processed in serial, you can set it to false and requests will be processed first by the order they're received and then by whatever request won't fail. For example:

Request 1
Request 2
Request 3
Request 4

Request 1 succeeds
Go to Request 2
Request 2 fails
Go to Request 3
Request 3 succeeds
Go to Request 4
Request 4 succeeds
Go to Request 2

@devj3ns
Copy link
Contributor Author

devj3ns commented Jul 18, 2024

Thanks for all this input @tshedor!

The way you describe the stateful queue (storing the response immediately in a separate table) is what I had in mind. I would store every response which has an HTTP Status Code >= 400, or every response which status code is in the reattemptForStatusCodes list. My first thought was to use the response status code or payload to determine if the request was blocked/rejected by the server, lock the request and give the user the ability to force or discard it. But as you said, it's probably the best to consider the UI first and then work back.

@devj3ns
Copy link
Contributor Author

devj3ns commented Jul 18, 2024

Regarding the serial processing: I am aware of the flag but the requests in my app, or to be more precise, some requests have to be processed in serial. This is due to the associations. Imagine a customers and a projects table. Every customer is associated with one or more projects. In this case, a request which creates a new customer has to be sent before a request which creates a new project. On the other hand, for two requests updating different projects, the order is not relevant. So I could not opt out of serial processing completely.

@devj3ns
Copy link
Contributor Author

devj3ns commented Sep 21, 2024

I finally had some time to revisit this topic and came up with a potential solution.

Quick Recap:

Problem: There’s no straightforward way in Brick to detect if a request sent by the OfflineQueueClient is rejected by the remote provider (e.g., due to a data conflict) and if the request queue is blocked as a result.

Context: In my app, upsert requests can be rejected by the database when a newer version of the dataset has been saved to the remote database in the meantime. This can occur when one user goes offline and edits data while another user modifies the same data. When the first user comes back online, the offline request queue becomes blocked as Brick keeps retrying the request indefinitely which always gets rejected.

Previously, I thought it would be necessary to make somewhat larger changes/adjustments to brick, such as storing responses in a separate local table or making the request queue more stateful. However, I now realize these changes might be unnecessary.

My new idea is to introduce an optional callback in the RestOfflineQueueClient that gets triggered when a request fails. Something like:

void Function(http.Request, http.StreamedResponse)? onRequestFailed;

This would allow me, within my application code, to check the currently attempted response HTTP code indicates a rejection (e.g., HTTP 409 Conflict). With that information, I could notify the user that the request was rejected and provide options on how to handle it (e.g., giving the user the chance to export the data and discard the request to unblock the queue).

(Additionally, we could extend this idea further by allowing the callback to return a reattempt boolean. This would let the application specify whether a request should be retried or discarded. For example, if the response code is 409, I could store the request and response elsewhere to resolve the conflict later, and return false to tell Brick to discard the request, thereby instantly unblocking the queue. This would be similar to the reattemptForStatusCodes list, but with the advantage of the application receiving the request/response pair before the request is discarded/reattempted.)

This is still a rough idea and not fully thought out, but @tshedor, what are your thoughts on this? I think this approach would give specific applications (like mine) the flexibility to handle such cases without requiring major changes to Brick, making it more suitable for the average user.

@tshedor
Copy link
Collaborator

tshedor commented Sep 21, 2024

@devj3ns I think this is actually a very good solution:

  1. It is not stateful
  2. It's optional
  3. It doesn't require a managed SQLite instance
  4. It isn't concerned with data sanitization

The RestOfflineRequestQueueClient was never really meant to be exposed, and in the repositories where it's implemented, it isn't available on the Repository level. However, RestOfflineRequestQueue is exposed on the repository, so I'd recommend creating an abstract method on OfflineRequestQueue, and implement it in RestOfflineRequestQueue and forward it to RestOfflineRequestQueueClient.

The only problem I imagine happening is StreamedResponse can only be parsed once. It throws an exception the second time you try to parse/read the body. So your method signature may need to account for that (and, based on the discussion in #440, http.Request may need to be looser). But these concerns shouldn't stop you from trying an initial implementation; you'd run into them in the unit tests if they are valid.

@devj3ns
Copy link
Contributor Author

devj3ns commented Sep 26, 2024

@tshedor, glad you like the idea!

I'd recommend creating an abstract method on OfflineRequestQueue, and implement it in RestOfflineRequestQueue and forward it to RestOfflineRequestQueueClient.

This approach requires the GraphqlOfflineRequestQueue to implement the method as well. I tried this, but ran into an issue I currently do not know how to resolve properly: the RestOfflineRequestQueueClient relies on the http.StreamedResponse type for a response, while GraphqlOfflineQueueLink uses the Response type.

To avoid this conflict, what do you think about introducing the method directly in RestOfflineQueueClient instead? We could then pass it through a new property in the OfflineFirstWithRestRepository constructor, which would forward it to RestOfflineQueueClient.

http.Request may need to be looser
With the approach described above, could maintain the http.Request type because the RestOfflineQueueClient only handles requests of type http.Request.

The only problem I imagine happening is StreamedResponse can only be parsed once
Yes, I'm aware of that. In my case, the HTTP status code provides enough information, so this limitation shouldn't be an issue.

I've opened a PR with my proposed implementation. I'd love to hear your thoughts and am happy to explore alternative solutions if needed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
2 participants