Closed alexandertokarev closed 9 months ago
Have no push permissions, so attached a patch file: Add_transaction_support.patch
Hi,
why not simply adding the @Transactional annotation to the resourceEndpoint
call? That was my approach up until now and it I haven't got any problems until now:
@Transactional
@RequestMapping(value = "/**", method = {RequestMethod.POST, RequestMethod.GET, RequestMethod.PUT,
RequestMethod.PATCH,
RequestMethod.DELETE}, produces = HttpHeader.SCIM_CONTENT_TYPE)
public @ResponseBody String handleScimRequest(HttpServletRequest request,
HttpServletResponse response,
@RequestBody(required = false) String requestBody)
{
Map<String, String> httpHeaders = getHttpHeaders(request);
String query = request.getQueryString() == null ? "" : "?" + request.getQueryString();
ScimAuthentication scimAuthentication = new ScimAuthentication();
ScimResponse scimResponse = resourceEndpoint.handleRequest(request.getRequestURL().toString() + query,
HttpMethod.valueOf(request.getMethod()),
requestBody,
httpHeaders,
new Context(scimAuthentication));
response.setContentType(HttpHeader.SCIM_CONTENT_TYPE);
scimResponse.getHttpHeaders().forEach(response::setHeader);
response.setStatus(scimResponse.getHttpStatus());
return scimResponse.toPrettyString();
}
this works great with ETags and BulkRequests. I use JPA with JTA transactions here and the behaviour is just as expected. I assume you made the ResourceHandler
itself a bean and you are trying to manage the transactions within the different methods?
Did you try my approach? Why is it not working in your case?
I do not want any misunderstandings here. I understand why you need getForUpdate
-method and the idea is a pretty good one. Did not think of it myself. But I am currently still doubting the benefit of the added Transactionmanager
.
Hi,
why not simply adding the @transactional annotation to the resourceEndpoint call?
I think this approach have some problems with rollback conditions. By default the transaction will roll back on RuntimeException and Error. But resourceEndpoint usually does not throw exceptions, it returns an error response instead, please see https://github.com/Captain-P-Goldfish/SCIM-SDK/blob/917110ca202ceb6575d89ce2bbea138b1f6efa28/scim-sdk-server/src/main/java/de/captaingoldfish/scim/sdk/server/endpoints/ResourceEndpoint.java#L196 So the transaction will always be commited even though DataAccessException is thrown by ResourceHandler
But this is not a big problem, we can be using TransactionTemplate and roll back transactions on getiing an ErrorResponse from resourceEndpoint
this works great with ETags and BulkRequests.
So all operations in a bulk request executed in the same transaction. This means either all the operations are successfull or all the operations are failed. Is it always a desired behaviour? Consider response contains the result of all processed operations, some operations have "status": "200" but the last one has "status": "400" In my opinion if we roll back the whole transaction we roll back changes in all operations in the bulk and "status": "200" for these operations is not a correct answer.
Also it would be great if we can mark transactions for getting resources as read only, allowing for corresponding optimizations at runtime. At the resourceEndpoint level we don't have info about the method, we have to parse url (listResources can be called via POST method for example)
I tried to give some flexibility to the end user by providing an appropriate TransactionManager from ResourceHandler. But may be there are some another good solutions, I only doubt the transaction markup at the controller level.
Okay lets go this through in detail:
RuntimeException
if not catched before the method annotated with @Transactional
because the transaction is handled in the surrounding spring-bean-wrapper.
resourceEndpoint
will not rollback on RuntimeException
because these are catched in the ResourceEndpointHandler
entityManager
throws a corresponding exception. This would mark the transaction immediately as rollback. (Point for you here)On BulkRequests
:
If the "failOnErrors" attribute is not specified or the service provider has not reached the error limit defined by the client, the service provider will continue to process all operations.
This behaviour is respected by the ResourceEndpoint
The rollback-only behaviour is indeed a problem but I think you might break the rule for BulkRequests
if you use it like this. Lets say you have a BulkRequest
with failOnErrors=2
. When you get the third error the whole transaction should be rolled back instead only part of it.
I will think a bit more over it today but in the end I guess there is also no harm in adding it. But I will probably rename the classes. These are more interceptors than transactionHandlers.
Good evening,
Lets say you have a BulkRequest with failOnErrors=2. When you get the third error the whole transaction should be rolled back instead only part of it.
I'm not sure that we should roll back the whole transaction. Also, I think we should not execute a bulk request in one transaction as well.
failOnErrors An integer specifying the number of errors that the service provider will accept before the operation is terminated and an error response is returned.
But I think this doesn't mean we should roll back previous successful operations in the bulk. In my opinion in case failOnErrors is reached we should skip processing further operations and return an error bulk response (status >= 400 ) that contains the result of all processed operations (successful and failed).
The service provider MUST continue performing as many changes as possible and disregard partial failures
The service provider response MUST include the result of all processed operations.
The status attribute includes information about the success or failure of one operation within the bulk job.
I believe we should execute each operation in the bulk in a separate transaction, let me explain why:
Let's say we store data in three tables:
We have the following implementation of UserHandler.updateResource method:
public User updateResource(User user, Context context) {
userDao.saveUser(user);
userToGroupDao.deleteAllGroupsForUser(user.getId()); // clear previous users_to_groups records
userToGroupDao.linkGroupsToUser(user.getId(), user.getGroups()); // insert records into users_to_groups table
}
We receive a bulk request with failOnErrors=10 and two PATCH operations for user1 and user2. This request will never reach failOnErrors. Let's say first operation was successful, but the second one failed as userToGroupDao.linkGroupsToUser method tried to insert non-existent groupId and violated foreign key constraint.
If we execute this bulk request in one transaction how should we end the transaction: commit or rollback?
if rollback we lost changes for user1 if commit we leave user2 in inconsistent state: user updated, previous users_to_groups records deleted but new users_to_groups records are not inserted
I have adjusted the commit and made the the transactionManager
an interceptor
. The readOnly
boolean is no longer given as parameter to the method getInterceptor
previously getTransactionManager
. Instead the EndpoinType
enum is given as parameter that should give a little bit more of control here.
I have added your commit unmodified. Like this you will be able to see the changes in the commit after.
This solution is better than the one I suggested. Interceptor can be used not only for transaction management, but also for other purposes, such as auditing. Great work, thanks a lot!
For server implementations that are using a transactional database to store the data all data operations (retrieving and modifying resource) should be executed within a transaction
Some operations can be made transactional in implementations, for example one can implement
UserHandler
and annotate some methods with spring@Transactional
annotation:But if we annotate the
updateResource
method:it would not be correct behaviour.
While PATCH/PUT request processing the
ResourceEndpointHandler
first callsResourceHandler.getResource
method to obtain previous resource version, then performs ETag validation, then applies patch operation against previous resource version and then callsResourceHandler.updateResource
All these steps should be executed within the same transaction. Also first call ofResourceHandler.getResource
method may want to lock the row to prevent access from parallel transactions, so thegetResource
method should be aware of its call purpose: whether the result will be used for further update operation or for sending to the client outputIn SQL the desired behaviour looks like as follows:
Consumers
doBeforeExecution
/doAfterExecution
are not fully siutable for this purpose. Now we can not begin transaction indoBeforeExecution
consumer and commit/rollback indoAfterExecution
consumer because of the following reasons:doBeforeExecution
called before each operation but thedoAfterExecution
called only once at the endgetResource
method knows nothing about call purposedoAfterExecution
consumer are not transferred toScimErrorResponse
Can not mark a transaction as read-only because
doBeforeExecution
consumer provides no info about methodWill provide a pull request