Spring Boot - how to avoid concurrent access to controller

Use synchronized - but if your user clicks quick enough then you will still have issues in that one command will execute immediately after another.

Synchronized will make sure only one thread executes the block in

synchronized(this) { ... } 

at a time.

You may also want to introduce delays and reject commands in quick succession.

try {
    synchronized(this) {
        String pinName = interruttore.getPinName();                     
            if (!interruttore.isStato()) { // switch is off
            GpioPinDigitalOutput relePin = interruttore.getGpio()
                .provisionDigitalOutputPin(RaspiPin.getPinByName(pinName));
            interruttoreService.toggleSwitchNew(relePin, interruttore, lit); // turn it on
            interruttore.getGpio().unprovisionPin(relePin);
       }
   }
} catch (GpioPinExistsException ge) {
    logger.error("Gpio già esistente");
}

This is a classical locking problem. You can either use pessimistic locking: by allowing only one client at the time to operate on the data (mutual exclusion) or by optimistic locking: by allowing multiple concurrent clients to operate on the data but allowing only the first committer to succeed.

There are many different ways to do that depending on the technology you are using. For example, an alternative way to solve it would be by using the right database isolation level. In your case it seems you need at least "repeatable read" isolation level.

Repeatable read will ensure that if two concurrent transactions read and change the same record more or less concurrently, only one of them will succeed.

In your case you could mark your Spring transaction with the right isolation level.

@Transacational(isolation=REPEATABLE_READ)
public void toggleSwitch() {
    String status = readSwithStatus();
    if(status.equals("on") {
         updateStatus("off");
    } else {
         updateStatus("on");
    }
}

If two concurrent clients try to update the switch status, the first to commit will win, and the second one will always fail. You just have to be prepared to tell the second client its transaction did not succeed due to concurrent failure. This second transaction is automatically rolled back. You or your client may decide to retry it or not.

@Autowire
LightService lightService;

@GET
public ResponseEntity<String> toggleLight(){
   try {
       lightService.toggleSwitch();
       //send a 200 OK
   }catch(OptimisticLockingFailureException e) {
      //send a Http status 409 Conflict!
   }
}

But as I was saying, depending on what you're using (e.g. JPA, Hibernate, plain JDBC), there are multiple ways to do this with either pessimistic or optimistic locking strategies.

Why Not Just Thread Synchronization?

Other answers suggested so far are about pessimistic locking by using Java's mutual exclusion at the thread level using synchronized blocks which might work if you have a single JVM running your code. This strategy might prove to be ineffective if you have more than one JVM running your code or if you eventually scale horizontally and add more JVM nodes behind a load balancer, in whose case thread locking would not solve your problem anymore.

But you could still implement pessimistic locking at the database level, by forcing the process to lock the database record before changing it and by this creating a mutual exclusion zone at the database level.

So, what matters here is understanding the locking principles and then finding a strategy that works for your particular scenario and tech stack. Most likely, in your case, it will involve some form of locking at the database level at some point.

Tags:

Java

Spring