0

I have a spring application which can run in a clustered environment. In that environment I use Redis (and Redisson) as a distributed lock-service.

Many of the locks are used to e.g. protect certain tasks which can only run once at a time, or which may only be started every X seconds.

The application is also capable of running in standalone mode (without redis).

However for this case I need a different implementation of the lockservice. I thought this would be extremely simple because I only need to create a Lock instance locally with a certain timeout (e.g. for the "only run action at most ever 2 minutes"). However when looking around I could not find any implementation of the Java Lock interface which supports setting a timeout for the lock (so that it automatically unlocks itself after that time).

Is there such a thing, or is there an extremely simple (in terms of lines-of-code) way how I can implement this myself, which I'm just missing?

How that lock impl should behave:

  • Other threads are not able to lock it while it's active (as any other lock)
  • Ideally, the owning thread should be able to call lock(long timoutMs) again to extend the lock (set the timeout to the given time again)

EDIT: It seems that a concrete example could help understand what I am looking for:

  • Imagine the server has an HTTP action "doExpesiveTask"
  • whenever this task is called, my application goes to its ockService" and calls .tryAcquireLock("expensiveTaskId", 10, TimeUnit.Minutes) and gets back a boolean if it got the lock or not.
  • if it got the lock it starts the task
  • if it didn't get the lock it doesn't and shows the user "you have to be more patient"

In a distributed setup the implementation of the lockService uses redis (and the Redisson library) distributed locks (this already works great)! To have a very simple switch between distributed and standalone mode, I simply want to have an implementation of lockService which doesn't rely on any external service. Therefore I would simply need an implementation of a Lock which supports a timeout. With that I could simply have a ConcurrentHashMap inside the lockservice which maps lock-ids to these lock instances.

Why not simply use a Map that maps lock-ids to time-objects: because I also need to prevent other threads from re-locking (extending the lifetime) of a lock which was acquired by another thread.

NoUsername
  • 693
  • 6
  • 20

4 Answers4

2

Your description is a little bit ambiguous, as you are talking about locks, but you are not actually locking a resource (or did not provide example). I feel your problem relates to scheduling.

Since you already use Spring, you could have a look at its scheduling options. Recent versions allow you to use @Scheduled annotation to trigger that. @EnableScheduling fires up background task executor. You could combo that with Spring profiles, to ensure these kick in only when you pass a profile, for example as a JVM parameter.

Copied from docs:

package hello;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ScheduledTasks {

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    @Scheduled(fixedRate = 5000)
    public void reportCurrentTime() {
        System.out.println("The time is now " + dateFormat.format(new Date()));
    }
}

and to enable:

package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Application {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(Application.class);
    }
}

There is a quick guide here:

Spring Docs

Service code (you would want to go with enumerators, used strings for clarity):

import org.apache.commons.collections4.map.PassiveExpiringMap;

public class StandAloneLockService {
    private Map ordinaryLocks;
    private Map expiringLocks;

    public StandAloneLockService() {
        this.ordinaryLocks = new HashMap<String, Long>();
        this.expiringLocks = new PassiveExpiringMap<String, Long>(2L,
                TimeUnit.MINUTES);
    }

    public synchronized boolean accquireLock(String task) {
        if (ordinaryLocks.containsKey("task")
                || expiringLocks.containsKey("task")) {
            return false;
        } else {
            return handle("task");
        }
    }

    private boolean handle(String jdk7) {
        switch (jdk7) { // logic 
        }
    }
    private void releaseLock(String task) {
        switch (task) { // logic 
        }
    }
}
Palcente
  • 625
  • 2
  • 7
  • 21
  • thank you for your answer, I am aware of sping scheduling (is already in use) however it seems I have to clarify my question again since this is also not what I am looking for (doesn't help in my case) – NoUsername Jul 23 '15 at 20:10
  • usage example added. Your scheduling example would be useful to trigger things in a recurring fashion in only "standalone" (single server) mode. When having a distributed setup, one would have to somehow write a custom scheduler (or maybe there is one for spring already). However what i'm looking for is something more in the direction of a rate-limiting setup. – NoUsername Jul 23 '15 at 20:19
  • I deduce from your added explaination, that you want to couple part of lock management to your tasks. If you do that, then you will add a complexity to your app. Imagine a task gets a "lock" and extends its "lifetime", it would need to be propagated across all threads. In fact, every change would need to be propagated and also acknowledged... How about if you decouple the lock extension logic to your lock service, and the task will only have the ability to run, based on whether they received a boolean indicator from the service. Let me add some code... – Palcente Jul 24 '15 at 09:21
  • > How about if you decouple the lock extension logic to your lock service, and the task will only have the ability to run, based on whether they received a boolean indicator from the service. That sounds like what I am trying to do. – NoUsername Jul 24 '15 at 12:11
  • And thanks for the code-snippet. the PassiveExpiringMap is already a step in the right direction. However it only supports one global lifetime. Since the actions can have different minimum-intervals that isn't enough. (Imagine a "synchronizeUserData" action can be triggered every 5 minutes, but a "synchronizeCompanyData" action can only be triggered every hour) – NoUsername Jul 24 '15 at 12:15
  • Two options here - depending on number of tasks, and how often they change... Either included that logic in your service(if maintenance is not an issue) or include that business logic in your enumerator, which you woud pass in as a parameter. Then maybe you would not even need passive expiring map if you chose to write it all up yourself... – Palcente Jul 24 '15 at 12:22
2

You can try putting the timeout in the ReentrantLock's await call for example:

public class MessageUtil {
  private static final Lock lock = new ReentrantLock();

  public enum Conditions {
     BAR_INIT(lock.newCondition()),
     TEST_DELAY(lock.newCondition());

     Condition condition;

     private Conditions(Condition condition) {
         this.condition = condition;
     }
  }

  public static void await(Conditions condition, int timeout) throws Interrupted Exception {
     lock.lock();
     condition.condition.await(timeout, TimeUnit.SECONDS);
     lock.unlock();
  }

  public static void signalAll(Conditions condtition) {
     lock.lock();
     condition.condition.signalAll();
     lock.unlock();
  }
}

This utility class allows you to use the await method to wait for a certain condition, say waiting for the Bar class to finish initializing or waiting for a certain step in a test then use the signalAll method to end the wait condition and resume normal operations.

0

There is a method in the Object class: public final void wait(long timeout). See http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#wait(long)

You can only call it in a synchronized block.

As the example (from the javadoc):

synchronized (obj) {
         while (<condition does not hold>)
             obj.wait(timeout);
         ... // Perform action appropriate to condition
     }
JFPicard
  • 5,029
  • 3
  • 19
  • 43
  • But that is waiting for the lock, that's not what i'm asking about. I'm asking about a lock which unlocks itself after a timeout. It should not be possible for other threads to lock it during that time. Ideally (I'll add that to my question now) it should be possible for the owning thread to call lock again to extend the lock by the given time. – NoUsername Jul 23 '15 at 12:57
  • Nothing. The thread is probably (hopefully ^^) already finished with its work. It may decide not to release the lock even if it finished earlier to prevent others from executing the same action again too soon. – NoUsername Jul 24 '15 at 12:16
0

If anyone is interested, this is the LockService implementation which I came up with for "clusterless" mode:

import com.google.common.collect.Maps;
import org.slf4j.Logger;

import java.lang.management.ThreadInfo;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static org.slf4j.LoggerFactory.getLogger;

public class LocalLockServiceImpl implements LockService {
    private static final Logger LOG = getLogger(ClusterLockServiceLocalImpl.class);

    private ConcurrentMap<String, TimeoutLock> lockMap = Maps.newConcurrentMap();
    private Object sync = new Object();

    @Override
    public boolean tryLockOrRelock(String lockName, long lockTimeMillis) {
        synchronized (sync) {
            TimeoutLock lock = lockMap.getOrDefault(lockName, new TimeoutLock());
            lockMap.put(lockName, lock);
            if (!lock.isExpired()) {
                if (!lock.isHeldByCurrentThread()) {
                    LOG.debug("cannot lock " + lockName + " because it is held by a different thread");
                    return false;
                }
            }
            lock.setExpiry(lockTimeMillis);
            return true;
        }
    }

    @Override
    public void unlock(String lockName) {
        synchronized (sync) {
            TimeoutLock lock = lockMap.getOrDefault(lockName, null);
            if (lock != null && lock.isHeldByCurrentThread()) {
                lockMap.remove(lockName);
            }
        }
    }

    private static class TimeoutLock {
        private LocalDateTime expiresAt;
        private long threadId;

        public TimeoutLock() {
            expiresAt = LocalDateTime.now();
            threadId = Thread.currentThread().getId();
        }

        public void setExpiry(long millisFromNow) {
            expiresAt = LocalDateTime.now().plus(millisFromNow, ChronoUnit.MILLIS);
        }

        public boolean isHeldByCurrentThread() {
            return threadId == Thread.currentThread().getId();
        }

        public boolean isExpired() {
            return expiresAt.isBefore(LocalDateTime.now());
        }
    }

}

So no real Locks used, the locking happens via the service and the TimeoutLock objects simply keep track of owning thread-id and timeouts. Wrote a few tests for it and so far everything looks good.

NoUsername
  • 693
  • 6
  • 20