Search code examples
javaspring-bootconcurrency

How can I solve the concurrency problem in a REST service in Spring Boot project?


I've a Spring Boot project.

Imagine two APIs that accept the same object as input to do different things with it. Before performing the action, each API validates the object, in particular there's a numeric field called progressiveNr, this number is basically used to understand the progression of requests for a specific voucher (without going into details, if it's not clear I'll discuss it).

Each API checks if the progressiveNr is correct based on the Voucher, so you can't send me progressiveNrs 1, 2, 4, 3 but it has to be sequentially.

That's what happened, the client called the 2 APIs milliseconds apart: API 1 {requestId: 196, voucherId: 1, progressiveNr: 2}, API 2 {requestId: 197, voucherId: 1, progressiveNr: 3}.

For requestId 197 we gave the error "progressiveNr must be 2". This is because, precisely while the 196 was processing, it was still in the validation phase and therefore had not yet performed any insert, the other API was called with the 197 with progressiveNr=3 which however, given that the 196 was not yet during the insert phase, the table rightly still showed progressiveNr=1 and therefore gave an error saying I expect progressiveNr=2.

Searching on the internet I found locks as a solution but I'm not clear how to apply it..

These are the methods called by the controller

    @Override
    @LogExecutionTime(printArgs = true, printResponse = true)
    public ResponseDTO subve(List<RequestSubveDTO> richieste) {
        List<ScartoTracciatoConnexa> daScartare = new ArrayList<>();

        //requests subve validation
        richieste.forEach(request -> {
            ScartoTracciatoConnexa sTC = validateSubve(request);

            if (sTC != null) daScartare.add(sTC);
        });

        //do other things

        //do the action
        asyncService.subve(richieste, daScartare);

        return new ResponseDTO(EsitiCodes.REQUEST_ACCEPTED);
    }

    private ScartoTracciatoConnexa validateSubve(RequestSubveDTO request) {
        try {
            validatorService.validateSubve(request);
        } catch (InvalidRequestException e) {
            return generaScarto(request, request.getNumVoucher(), SUBVE, e);
        }

        return null;
    }

    @Override
    @LogExecutionTime(printArgs = true, printResponse = true)
    public ResponseDTO renew(List<RequestRenewDTO> richieste) {
        List<ScartoTracciatoConnexa> daScartare = new ArrayList<>();

        //requests renew validation
        richieste.forEach(request -> {
            ScartoTracciatoConnexa sTC = validateRenew(request);

            if (sTC != null) daScartare.add(sTC);
        });

        //do other things

        //do the action
        asyncService.renew(richieste, daScartare);

        return new ResponseDTO(EsitiCodes.REQUEST_ACCEPTED);
    }

    private ScartoTracciatoConnexa validateRenew(RequestRenewDTO request) {
        try {
            validatorService.validateRenew(request);
        } catch (InvalidRequestException e) {
            return generaScarto(request, request.getNumVoucher(), RENEW, e);
        }

        return null;
    }

and this is my validatorService:

    @LogExecutionTime
    @Transactional(readOnly = true)
    public void validateRenew(RequestRenewDTO dto) throws InvalidRequestException {
        //Other validations

        //progressiveNr validation
        int lastProgNr = myObjectService.getLastProgNumByVoucherId(dto.getVoucherId()) + 1;

        if (lastProgNr != dto.getProgressiveNr())
            throw new InvalidRequestException();
    }

    //THE SAME FOR validateSubve(RequestSubveDTO dto)

Can anyone help me? Thanks in advance!


Solution

  • It is synchronous calls, it shouldn't be possible for your client to break the contract. It broke it if both request were sent at the same time and not synchronously. A client shouldn't be able to decide the state of your voucher without asking for it first.

    So you answer a 400 Bad Request for the requestId 197 and 200 OK for the requestId196. You should not struggle to implement bizarre stuff trying to guess what your client was willing to achieve.