Giter Site home page Giter Site logo

distributed-lock's Introduction

Java JitPack Spring Boot License

Distributed Lock

Distributed lock ensures your method cannot be run in parallel from multiple JVMs (cluster of servers, microservices, …​). It uses a common store to keep track of used locks and your method needs to acquire one or more locks to run.

By default, locks follow methods lifecycle.They are obtained at the start of the method and released at the end of the method. Manual controlling is supported and explained later in this document.

All locks acquired by lock implementations in this project will expire after 10 seconds, timeout after 1 second if unable to acquire lock and sleep for 50 ms between retries. These options are customizable per annotation.

Using locks

To lock your methods you need to first enable locking as described in the previous section.

Spring BeanPostProcessor will handle all @Locked methods including their aliases. The type field describes which implementation of the lock to use. To prevent repeating yourself if you plan on using the same implementation (as most people usually will), I’ve added alias support. They wrap the @Locked annotation and define the type used.

Each lock needs to define a SpEL expression used to acquire the lock. To learn more about Spring aliases visit this link.

By default, upon failure to acquire the lock @Locked will throw DistributedLockException. If you need to only log this failure without raising the exception add throwing = false to your @Locked annotation. Using this option on non-void methods will make the method return null - using primitives as return type with this option is not advised. This is useful if you need to lock an action across multiple application instances, for example cron.

Lock refresh

Locks can be refreshed automatically on a regular interval. This allows methods that occasionally need to run longer than their expiration. Refreshing the lock periodically prolongs the expiration of its key(s). This means that the lock cannot be acquired by another resource as long as the resource using the lock does not end successfully. In case the resource holding the lock fails unexpectedly without releasing the lock, the lock will expire according to the last expiration that was written (that the last refresh has set).

Manually controlled locks

Sometimes you might want lock to be acquired when calling a specific method and get released only when it expires (throttling).

To acquire a lock that doesn’t get released automatically set manuallyReleased to true on @Locked annotation.

For more grained control (e.g., locking in the middle of the method and releasing later in the code), inject the lock in your service and acquire the lock manually.

Example

@Component
public class Example {

    @Autowired
    @Qualifier("simpleRedisLock")
    private Lock lock;

    // other fields...

    private void manuallyLocked() {
        // code before locking...

        final String token = lock.acquire(keys, storeId, expiration);

        // check if you acquired a token
        if (StringUtils.isEmpty(token)) {
            throw new IllegalStateException("Lock not acquired!");
        }

        // code after locking...

        lock.release(keys, token, storeId);

        // code after releasing the lock...
    }
}

Unsuccessful locks

If method cannot be locked, DistributedLockException will be thrown.

Method might not acquire the lock if:

  1. keys from SpEL expression cannot be resolved

  2. another method acquired the lock

  3. Lock implementation threw an exception

Examples

Locking a method with the name aliased in the document called lock in MongoDB:

@MongoLocked(expression = "'aliased'", storeId = "distributed_lock")
public void runLockedWithMongo() {
    // locked code
}

Locking with multiple keys determined in runtime, use SpEL, for an example:

@RedisMultiLocked(expression = "T(com.example.MyUtils).getNamesWithId(#p0)")
public void runLockedWithRedis(final int id) {
    // locked code
}

This means that the runLockedWithRedis method will execute only if all keys evaluated by expression were acquired.

Locking with a custom lock implementation based on value of integer field count:

@Locked(type = MyCustomLock.class, expression = "getCount", prefix = "using:")
public void runLockedWithMyCustomLock() {
    // locked code
}

Enabling locking

The project contains several configurations and annotations to help you enable locking and customize it.

To enable locking you must first include @EnableDistributedLock. This will import the configuration that will autoconfigure the BeanPostProcessor required for locking.

Project provides the following out-of-the-box lock implementations:

  • JDBC

  • Mongo

  • Redis

JDBC locks

JDBC locks are provided in the distributed-lock-jdbc project.

Table 1. Mongo lock implementations
Implementation Alias Multiple key support

SimpleJdbcLock

@JdbcLocked

No

Include @EnableJdbcDistributedLock to enable JDBC locks. This will also include @EnableDistributedLock for you.

@Configuration
@EnableJdbcDistributedLock
public class LockConfiguration {
}
Note

Make sure you create the table and configure the table ID incrementer.

Example how to create table:

CREATE TABLE IF NOT EXISTS `distributed_lock` (
    id       INT NOT NULL AUTO_INCREMENT,
    lock_key VARCHAR(255),
    token    VARCHAR(255),
    expireAt TIMESTAMP,
    PRIMARY KEY(`id`),
    UNIQUE KEY `uk_lock_lock_key` (`lock_key`)
);

MongoDB locks

MongoDB locks are provided in the distributed-lock-mongo project.

Table 2. Mongo lock implementations
Implementation Alias Multiple key support

SimpleMongoLock

@MongoLocked

No

Include @EnableMongoDistributedLock to enable MongoDB locks. This will also include @EnableDistributedLock for you.

@Configuration
@EnableMongoDistributedLock
public class LockConfiguration {
}
Note

Make sure you create TTL index in your @Locked#storeId() collection on expireAt field to enable lock expiration.

Redis locks

Redis locks are provided in the distributed-lock-redis project.

Table 3. Redis lock implementations
Implementation Alias Multiple key support

SimpleRedisLock

@RedisLocked

No

MultiRedisLock

@RedisMultiLocked

Yes

Include @EnableRedisDistributedLock to enable Redis locks. This will also include @EnableDistributedLock for you.

@Configuration
@EnableRedisDistributedLock
public class LockConfiguration {
}

Importing into your project

Maven

Add the JitPack repository into your pom.xml.

<repositories>
  <repository>
    <id>jitpack.io</id>
    <url>https://jitpack.io</url>
  </repository>
</repositories>

JitPack builds multi-modules by appending the repo name in the groupId. To add the Redis dependency for an example, add the following under your <dependencies>:

<dependencies>
  <dependency>
    <groupId>com.github.alturkovic.distributed-lock</groupId>
    <artifactId>distributed-lock-redis</artifactId>
    <version>[insert latest version here]</version>
  </dependency>
</dependencies>

Compatibility

Fully compatible with Spring 3. For earlier version support check the compatibility table below. Older versions will not be maintained or bugfixed.

Version Spring Boot version

2.0.0+

3.1.5

1.4.1+

2.4.3

1.3.0+

2.2.7.RELEASE

1.2.0+

2.1.0.RELEASE

1.1.8+

2.0.4.RELEASE

1.1.7+

2.0.3.RELEASE

1.1.6-

1.5.6.RELEASE

SpEL key generator

This is the default key generator the advice uses. If you wish to use your own, simply write your own and define it as a @Bean.

The default key generator has access to the currently executing context, meaning you can access your fields and methods from SpEL. It uses the DefaultParameterNameDiscoverer to discover parameter names, so you can access your parameters in several different ways:

  1. using p# syntax, where # is the position of the parameter, for an example: p0 for the first parameter

  2. using a# syntax, where # is the position of the parameter, for an example: a2 for the third parameter

  3. using the parameter name, for an example, #message — REQUIRES -parameters compiler flag

A special variable named executionPath is used to define the method called. This is the default expression used to describe the annotated method.

All validated expressions that result in an Iterable or an array will be converted to List<String> and all other values will be wrapped with Collections.singletonList. Elements of Iterable or array will also be converted to Strings using the ConversionService. Custom converters can be registered. More about Spring conversion can be found here.

For more examples, take a look at com.github.alturkovic.lock.key.SpelKeyGeneratorTest.

Customization

If you want to use custom lock implementations, simply implement Lock interface and register it in a configuration. You can also create an alias for your lock so you don’t have to specify @Locked type field.

Changelog

Started tracking the changes since 1.2.0 so no changelogs available for earlier versions.

2.1.0

  • FEATURE: Added option throwing to @Locked annotations

2.0.0

  • CHANGE: Upgraded Spring Boot version to 3.1.5

1.5.5

  • BUGFIX: Add initial refresh delay to avoid calling refresh immediately

  • BUGFIX: Changed default storeId to distributed_lock

1.5.4

  • BUGFIX: Do not execute locked method if token is not acquired after all retries

1.5.3

  • BUGFIX: RetriableLock should return null if lock is not acquired after the last retry

1.5.2

  • BUGFIX: Use dedicated task scheduler for DistributedLock, avoid trying to override custom default scheduler

1.5.1

  • BUGFIX: Removed semicolon from SQL statements for PSQL compatibility

1.5.0

  • CHANGE: Changed the default SQL table name from lock to distributed_lock to avoid issues with reserved database keywords

1.4.4

  • BUGFIX: No retries will be attempted if retry or timeout are zero or negative

  • BUGFIX: Handle Redis interruptions in Redis locks better

  • BUGFIX: SQL script updated in README

1.4.3

  • BUGFIX: Use Spring scheduler if enabled instead of overriding

  • BUGFIX: Escape lock keyword in SQL locks since MySQL uses it as a keyword

1.4.2

  • CHANGE: KeyGenerator will not declare ConversionService but reuse the shared instance if missing

1.4.1

  • CHANGE: Upgraded Spring Boot version to 2.4.3

  • CHANGE: Migrated test to JUnit 5

  • CHANGE: Migrated Redis tests to use Docker container

  • BUGFIX: Injecting the user-defined LockTypeResolver properly

  • BUGFIX: Fixed BeanPostProcessor initialization warning messages

  • BUGFIX: Minor javadoc fix

1.4.0

  • CHANGE: Switched back to Java 1.8 from 11 since most projects don’t yet use 11

1.3.0

  • CHANGE: Updated Java from 1.8 to 11

  • CHANGE: Refactored lots of coupled code

  • CHANGE: Extracted lots of reusable components such as retriable locks for easier manual control of locks

  • BUGFIX: LockBeanPostProcessor will now fire after existing advisors to support transactional advisors

1.2.2

  • CHANGE: Removed explicit ParameterNameDiscoverer from SpelKeyGenerator which now uses the one provided by the CachedExpressionEvaluator

  • CHANGE: Used AopUtils once and passed the evaluated method to SpelKeyGenerator so it doesn’t have to evaluate the same thing as LockMethodInterceptor

1.2.1

  • FEATURE: Lock refreshing has been added. Check the 'Lock refresh' chapter for more details

  • BUGFIX: @RedisMultiLocked was using #executionPath as prefix instead of an expression

  • BUGFIX: @RedisMultiLocked was using expiration and timeout in milliseconds instead of seconds

1.2.0

  • FEATURE: Added a JavaDoc description to com.github.alturkovic.lock.Lock.release() method

  • CHANGE: Rearranged the parameters of the com.github.alturkovic.lock.Lock.release() method to be more consistent

  • CHANGE: Rearranged the parameters of the com.github.alturkovic.lock.jdbc.service.JdbcLockSingleKeyService methods to be more consistent

  • CHANGE: EvaluationConvertException and LockNotAvailableException now extend the DistributedLockException

distributed-lock's People

Contributors

alturkovic avatar ctamisier avatar davidpsterling avatar gstaykov avatar kuanglx avatar matiasf avatar middagj avatar natureknight avatar ryeon9445 avatar sguillope avatar tomas-c avatar unbearables avatar zacksleo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

distributed-lock's Issues

Getting Started

Hi @alturkovic and co.,

Thank you for creating this project - I'm a recently graduated student and am looking to learn more about locks and concurrency for an upcoming project I'm doing. This has been very inspiring and cool!

I have been having some issues getting this setup and in understanding how to run the examples and test cases in the project.

I have cloned the repo, and attempted to use "mvn spring-boot:run" to run the examples but I get a host of errors. I am not sure exactly how to run the examples or test cases and would appreciate any guidance you might be able to give. I.e. does the project contain a dummy or example distributed system that this locking tool is used on?

Thank you!

Null lock token versus LockNotAvailableException

Hi,

when acquiring a lock (manually), there seems to be some confusion as to what should happen when the lock is not acquired.

The documentation on the Lock interface states, that null will be returned from acquire(), but both implementations seem to be throwing an exception (LockNotAvailableException).

So I am not sure what to do: If I rely in the interface, my code will fail. If I rely on the implementation, it may change and fail, just in reverse.

Am I missing something?

Spring Boot 2.2

Any plans to release a version that upgrades the Spring Boot dependency to 2.2?

Unable to Pull New Version from Jitpack

Thanks for certifying the project on the latest version of the Spring Boot framework. Unfortunately, I am unable to pull the latest version for any of the packages. If I try to pull the following as specified by Jitpack, then I get an auth error.

<dependency>
    <groupId>com.github.alturkovic</groupId>
    <artifactId>distributed-lock</artifactId>
    <version>1.3.0</version>
</dependency>

I am still able to pull 1.2.2 without issue.

What is the reason behind only using JitPack?

I was about to start using this library for a project at the company I work for. But the first link on Google points to an issue in the Gradle repo where it is argued against using JitPack due to multiple security concerns. This is a show stopper for my company at least. And I imagine for many others as well. I can see that previous version were published on Maven Central. So the question is why stop using it?

JDBC lock not working with PSQL due to SQL syntax

Hi, the recent changes to the SQL query for MySql is causing PSQL syntax errors.

  public static final String ACQUIRE_FORMATTED_QUERY = "INSERT INTO `%s` (lock_key, token, expireAt) VALUES (?, ?, ?);";
  public static final String RELEASE_FORMATTED_QUERY = "DELETE FROM `%s` WHERE lock_key = ? AND token = ?;";
  public static final String DELETE_EXPIRED_FORMATTED_QUERY = "DELETE FROM `%s` WHERE expireAt < ?;";
  public static final String REFRESH_FORMATTED_QUERY = "UPDATE `%s` SET expireAt = ? WHERE lock_key = ? AND token = ?;";

It seems to be that the inclusion of " ` " is breaking PSQL syntax.

org.postgresql.util.PSQLException: ERROR: syntax error at or near "`"

Not too sure on a fix, but it looks like MySql and PSQL syntax are conflicting, would appreciate if you could look into this.
Thanks.

Testing

Is there an easy way to test annotated methods in SpringBootTest?

I'd like to disable distributed-lock entirely, I tried with:

@Profile("!test")
@Configuration
@EnableJdbcDistributedLock
class DistributedLockConfiguration

But this fails at runtime

Need a hand :) !

Hello!

I'm trying to use manually acquired and released locks, for a project that needs to release locks in other threads and async methods.
But, with the example code of acquiring :

@Qualifier("simpleMongoLock")
    @Autowired
    private final Lock lock;       
    private String acquireLock(String id) {
        final String token = lock.acquire(
                Collections.singletonList(id),
                "locks",
                LOCK_EXPIRATION
        );
        if (StringUtils.isEmpty(token)) {
            throw new IllegalStateException("Lock not acquired for id" + id);
        }
        return token;
    }

    public void releaseLock(String id, String token) {
        lock.release(
                Collections.singletonList(id),
                token,
                "locks"
        );
    }

And configuration :

import com.github.alturkovic.lock.mongo.configuration.EnableMongoDistributedLock;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableMongoDistributedLock
public class LockConfiguration {

}

I'm able to use distributed locking with annotations @MongoLocked, but not manually released, the lock never gets deleted from database, and even if it's expired, I'm not able to acquire it again !

And in the logs I see that when I try to release the lock just after acquiring it. The acquire is successful, but not the release!

c.g.a.lock.mongo.impl.SimpleMongoLock : Remove query did not affect any records for key

Does anybody ever tried this ?

Thank you!!

Option not to throw exception on lock fail

Hi,

We have a use case where we run a cron on a Spring service spawned in multiple instances and we need to run it only on one instance. For this we use @RedisLocked and we expect that all of the locks will fail BUT the winning one. This is working now as expected, but the failing ones throw DistributedLockException in LockMethodInterceptor.invoke from method executeLockedMethod.

This is not the ideal case for us, as these exceptions are hard to catch in spring (@RedisLocked is on the same level as @Scheduled) and are not welcome in our usecase.

It would be nice to have an option on @RedisLocked annotation to suppress this exception and log it in warn instead. For example @RedisLocked(logFailedLock = true) or @RedisLocked(suppressFailure = true).

Cron is not the only usecase, this option can help any usecase that needs to lock across multiple instances without raising exceptions.

I am prepared to create PR for you.

Software license

I would love to use this library in one of our projects, but unfortunately there is no software license available in the repository. Therefore I can not use it. Would you be open to publishing this code under Apache 2.0, MIT or something similar?

More detailed examples

I'm trying to lock a couple of methods across jvm instances using Jdbc and Postgres.

So what I'm doing:

  • @EnableJdbcDistributedLock
  • creating the lock table:
CREATE TABLE lock
(
  id SERIAL,
  key VARCHAR(255) UNIQUE,
  token VARCHAR(255),
  expireAt TIMESTAMP
);
  • annotating my method with @JdbcLocked

But it doesn't work: Unable to acquire lock with expression: #executionPath.

I'd like to use simply the name of the method as a key, for start.
But I'm not really sure how to move from here 😅

Could we have retry and timeout added to simple locks for manual use

I used the following code stripped form the LockMethodInterceptor to get the retry/timeout functionality with a simple redis lock. It sure would be nice if it was part of the library.

`
private String getLockToken(List keys, String storeId, int retry, long timeOut, long expiration) {

    String token = null;
    try {
        token = constructRetryTemplate(retry, timeOut).execute(context -> {
            final String attemptedToken = lock.acquire(keys, storeId, expiration);

            if (StringUtils.isEmpty(attemptedToken)) {
                throw new LockNotAvailableException(
                        String.format("Lock not available for keys: %s in store %s", keys, storeId));
            }

            return attemptedToken;
        });
    } catch (final Exception e) {
        throw new DistributedLockException(String.format("Unable to acquire lock for keys: %s in store %s", keys, storeId), e);
    }
    return token;
}

private RetryTemplate constructRetryTemplate(int retryPeriodMs, long timeOutMs) {
    // how long to sleep between retries
    final FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
    fixedBackOffPolicy.setBackOffPeriod(retryPeriodMs);

    // when to timeout the whole operation
    final TimeoutRetryPolicy timeoutRetryPolicy = new TimeoutRetryPolicy();
    timeoutRetryPolicy.setTimeout(timeOutMs);

    // what exceptions to retry; only LockNotAvailableException, all other exceptions are unexpected and locking should fail
    final SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy(Integer.MAX_VALUE, Collections.singletonMap(LockNotAvailableException.class, true));

    // combine policies
    final CompositeRetryPolicy compositeRetryPolicy = new CompositeRetryPolicy();
    compositeRetryPolicy.setPolicies(new RetryPolicy[]{timeoutRetryPolicy, simpleRetryPolicy});

    // construct the template
    final RetryTemplate retryTemplate = new RetryTemplate();
    retryTemplate.setRetryPolicy(compositeRetryPolicy);
    retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

    return retryTemplate;
  }

`

Would be nice to have Nullability annotations

For example I didn't realize that distributedLock.acquire(keyList, STORE_ID, expiration) could actually return null until it did.

Nullability annotations would make it nicer to work with this library from Kotlin, for example.

Of course not a deal breaker 😄 .

About throwing option added in recent PR

I was wondering why line 65 in the code block below is returning null instead of returning the result of the invocation.proceed() call.

} catch (DistributedLockException e) {
if (context.getLocked() != null && !context.getLocked().throwing()) {
log.warn("Cannot obtain lock for keys {} in store {}", context.getKeys(), context.getLocked().storeId(), e);
return null;
}
throw e;
} finally {

I assumed that the throwing option was added to call the target method rather than throwing a DistributedLockException when there's a problem with redis or a network failure, but seeing that it returns null, I'm wondering if my assumption is wrong.

Importing the whole library causes auto wiring issues in Spring Boot

The Problem
Using one of the out-of-the-box lock implementations, e.g. JDBC, BUT importing the whole library as per the readme, i.e.

<dependencies>
  <dependency>
    <groupId>com.github.alturkovic.distributed-lock</groupId>
    <artifactId>distributed-lock-redis</artifactId>
    <version>[insert latest version here]</version>
  </dependency>
</dependencies>

causes Spring Boot auto wiring issues during startup for the other implementations, e.g.1

"Spring Data MongoDB - Could not safely identify store assignment for repository candidate interface uk.co.news.applenews.respository.AppleNewsFailedReceiptRepository. If you want this repository to be a MongoDB repository, consider annotating your entities with one of these annotations: org.springframework.data.mongodb.core.mapping.Document (preferred), or consider extending one of the following types with your repository: org.springframework.data.mongodb.repository.MongoRepository."

and e.g.2

"Spring Data Redis - Could not safely identify store assignment for repository candidate interface uk.co.news.applenews.respository.AppleNewsFailedReceiptRepository. If you want this repository to be a Redis repository, consider annotating your entities with one of these annotations: org.springframework.data.redis.core.RedisHash (preferred), or consider extending one of the following types with your repository: org.springframework.data.keyvalue.repository.KeyValueRepository."

Suggested Solution
Exclude the out-of-the-box lock implementations that aren't needed, e.g.

    implementation('com.github.alturkovic:distributed-lock:1.5.5'){
        exclude module: 'distributed-lock-example'
        exclude module: 'distributed-lock-mongo'
        exclude module: 'distributed-lock-redis'
    }

RedisLock throws RedisReadOnlyException when using RedisStaticMasterReplicaConfiguration and ReadFrom.REPLICA_PREFERRED

Hi!
I'm using a master/replica scheme with "read from replica" client property enabled.

var masterReplicaConfiguration =  new RedisStaticMasterReplicaConfiguration(masterNode.getHost(), masterNode.getPort());
redisProperties
    .getReplicas()
    .forEach(
        replica -> {
          RedisNode node = RedisNode.fromString(replica.getHost());
          masterReplicaConfiguration.addNode(node.getHost(), node.getPort());
        });

LettuceClientConfiguration.LettuceClientConfigurationBuilder clientConfigBuilder =
    LettuceClientConfiguration.builder().readFrom(ReadFrom.REPLICA_PREFERRED);

It seems that "execute" is treated as a read operation by default, so it throws:

c.github.alturkovic.lock.exception.DistributedLockException: Unable to acquire lock with expression: #key
	at com.github.alturkovic.lock.advice.LockMethodInterceptor.executeLockedMethod(LockMethodInterceptor.java:77)
	at com.github.alturkovic.lock.advice.LockMethodInterceptor.invoke(LockMethodInterceptor.java:61)
	... 3 frames excluded
	...
Caused by: <#029e4a23> org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisReadOnlyException: READONLY You can't write against a read only replica. script: 15df23044ffd4f2723d9689555be4a5cccac4154, on @user_script:1.
	at o.s.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:54)

JDBC Manual Controlled Lock

Hi @alturkovic ,

Want to ask regarding the manually controlled locks, i saw the sample code as below but the method lock.acquire(keys, storeId, expiration, retry, timeout) is missing. I cant find acquire method with 5 params. Is it missing method on it ?

And May i know does it possible have dedicated method to check whether it is locked instead of use the lock.acquired method to check on return token value whether it is empty ? Appreciate your help. Thanks

@component
public class Example {

@Autowired
@Qualifier("simpleRedisLock")
private Lock lock;

// other fields...

private void manuallyLocked() {
    // code before locking...

    final String token = lock.acquire(keys, storeId, expiration, retry, timeout);

    // check if you acquired a token
    if (StringUtils.isEmpty(token)) {
        throw new IllegalStateException("Lock not acquired!");
    }

    // code after locking...

    lock.release(keys, token, storeId);

    // code after releasing the lock...
}

}

JDBC lock is not working with MySql due to wrong SQL syntax

I have created SQL table using the following script from the documentation:

CREATE TABLE lock (
    id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
    lock_key varchar(255) UNIQUE,
    token varchar(255),
    expireAt TIMESTAMP,
    PRIMARY KEY(`id`)
);

After that I'm using the following code:

@JdbcLocked(prefix = "accountId", expression = "#accountId", storeId = "`lock`")

This result in the following exception:

java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'lock WHERE expireAt < '2022-08-21 19:20:56.016'' at line 1
	...
	com.github.alturkovic.lock.jdbc.service.SimpleJdbcLockSingleKeyService.acquire(SimpleJdbcLockSingleKeyService.java:51)
	at com.github.alturkovic.lock.jdbc.service.SimpleJdbcLockSingleKeyService$$FastClassBySpringCGLIB$$7d90f809.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	...

I manage to fix this in my code by overriding storeId:

@JdbcLocked(prefix = "accountId", expression = "#accountId", storeId = "`lock`")

In MySql 'lock' is keyword so if you are using this for table name you need to escape it. So every SQL query in SimpleJdbcLockSingleKeyService need to be scaped with `

like this:

  public static final String ACQUIRE_FORMATTED_QUERY = "INSERT INTO `%s` (lock_key, token, expireAt) VALUES (?, ?, ?);";
  public static final String RELEASE_FORMATTED_QUERY = "DELETE FROM `%s` WHERE lock_key = ? AND token = ?;";
  public static final String DELETE_EXPIRED_FORMATTED_QUERY = "DELETE FROM `%s` WHERE expireAt < ?;";
  public static final String REFRESH_FORMATTED_QUERY = "UPDATE `%s` SET expireAt = ? WHERE lock_key = ? AND token = ?;";

I will create a pull request for this

Mongo distributed lock - expireAt not used

Hi,

Thanks for the implementation but I am wondering when the "expired" lock document are removed in the following code

final LockDocument doc = mongoTemplate.findAndModify(query, update, options, LockDocument.class, storeId);

Am I missing something?

By the way, I think I'll adapt your implementation to use ReactiveMongoTemplate.

Allow re-entrant code within same Thread?

Currently a single Thread of execution cannot call the same @Locked method twice (or more) with the same parameter values. I think maybe this should be allowed as the Thread already 'owns' the first lock? Java's synchronized allows this.

It's happening to us where we override a @Locked method in a subclass and call super.method(), and it looks like distributed-lock's advice is run twice - first for the subclass's method call and then again when it calls its parent class's method with super.method().

Testing Annotated Locks

Hi,

I'm struggling to test the annotated locks. I've tried everything under the sun to get it to work but I cannot seem to get anything working.

It doesn't seem the annotation is working what so ever under test conditions (as evidenced by the fact, even when changing the storeId to one that doesn't exist nothing happens).

I've tried annotating the test class with @EnableJdbcDistributedLock even though my Application file already has this annotation. When the method is called it is not triggering the annotation at all.

I've tried annotating the method that I would like locked with both @Locked and @JdbcLocked of which neither made any difference.

I tried to create a very simple test to remove any doubt:

    @JdbcLocked(expression = "#p0")
    public void fakeMethod(UUID id) {
        log.debug("Fake method called! {}", id);
    }
    @Test
    public void simpleTest() {
        testService.fakeMethod(UUID.randomUUID());
    }

This should fail, since I don't even have a DB table available for this lock, yet it is not doing anything.

Any advice for testing the annotations would be greatly appreciated.

Spring Boot version 3.1.9 (tested with 3.1.5 too just in case)
Distributed Lock Version: 'com.github.alturkovic.distributed-lock:distributed-lock-jdbc:2.1.0'

Need a help

Dear Alen,

I've made a distributed-lock spring boot starter, replaced Lock implementation to redisson, but when i integrated it to my example project, it failed with an error which confused me a lot.
It seems that if LockBeanPostProcessor is instantiated before WebMvcAutoConfiguration, then this error would occur. And also, I've tried excluding AutoConfiguration from spring.factories files and using @EnableXXX, then it will be OK, i.e. LockBeanPostProcessor instance should be instantiated after WebMvcAutoConfiguration loaded.
From you doc, @EnableJdbcDistributedLock would be annotated in project, i wonder if this is the same situation.
I wanna if you could help me find a solution. Thx.

The components used
  • spring-boot-starter-web 2.3.7
  • redisson-spring-boot-starter 3.14.1
  • distributed-lock 1.4.0
The error before server started
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'resourceHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.HandlerMapping]: Factory method 'resourceHandlerMapping' threw exception; nested exception is java.lang.IllegalStateException: No ServletContext set
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:657) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:637) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1336) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1176) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879) ~[spring-context-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551) ~[spring-context-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143) ~[spring-boot-2.3.7.RELEASE.jar:2.3.7.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758) [spring-boot-2.3.7.RELEASE.jar:2.3.7.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750) [spring-boot-2.3.7.RELEASE.jar:2.3.7.RELEASE]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:405) [spring-boot-2.3.7.RELEASE.jar:2.3.7.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) [spring-boot-2.3.7.RELEASE.jar:2.3.7.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237) [spring-boot-2.3.7.RELEASE.jar:2.3.7.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.3.7.RELEASE.jar:2.3.7.RELEASE]
	at cn.hrfax.redisson.RedissonApplication.main(RedissonApplication.java:17) [classes/:na]
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.HandlerMapping]: Factory method 'resourceHandlerMapping' threw exception; nested exception is java.lang.IllegalStateException: No ServletContext set
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:652) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	... 20 common frames omitted
Caused by: java.lang.IllegalStateException: No ServletContext set
	at org.springframework.util.Assert.state(Assert.java:76) ~[spring-core-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.resourceHandlerMapping(WebMvcConfigurationSupport.java:534) ~[spring-webmvc-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_261]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_261]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_261]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_261]
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	... 21 common frames omitted
Part of codes

image
image

Sincerely
Selwyn

`scriptExecutor` of `RedisTempate` is null

Hello,

I am getting this weird issue where scriptExecutor of RedisTempate when using the lock is null and hence creation of the lock is giving me a NPE. Here is how I create the lock

Lock Config

@Configuration
@EnableRedisDistributedLock
public class LockConfiguration {

    @Bean
    public Lock lock(@Value("${redis.lock.secret}") String secret) {
        return new SimpleRedisLock(() -> secret, new StringRedisTemplate());
    }
}

Cache Config

@Log4j2
@Configuration
public class CacheConfiguration {

    @Bean
    JedisConnectionFactory jedisConnectionFactory(@Value("${redis.host}") String host, @Value("${redis.port}") int port) {
        log.info("Connecting to Redis at: " + host);
        return new JedisConnectionFactory(new RedisStandaloneConfiguration(host, port));
    }

    /**
     * Configures template for serialization and deserialization of cache entries.
     * @return redis template
     */
    @Bean
    public RedisTemplate<String, List<String>> redisTemplate(@Value("${redis.host}") String host, @Value("${redis.port}") int port) {
        RedisTemplate<String, List<String>> template = new RedisTemplate<>();
        template.setValueSerializer(RedisSerializer.json());
        template.setKeySerializer(RedisSerializer.string());
        template.setConnectionFactory(jedisConnectionFactory(host, port));
        return template;
    }
}

Usage of lock

@Log4j2
@Repository
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class EventKeysCache {

    private final RedisTemplate<String, String> redisTemplate;
    private final StringRedisTemplate stringRedisTemplate;
    private final Lock lock;
    @Value("${redis.lock.expiration}")
    private Integer expiration;
    @Value("${redis.lock.secret}")
    private String lockSecret;
    @Value("${redis.lock.collection}")
    private String lockCollection;


    public void createLock(String eventFilter) {
        log.info("Creating lock for eventFilter: " + eventFilter);
        try {
            lock.acquire(Collections.singletonList(eventFilter), lockCollection, expiration);
        } catch (Exception e) {
            log.info("Failed to acquire lock for eventFilter: " + eventFilter + e.getMessage() + e.getStackTrace().toString());
        }
    }

    public void releaseLock(String eventFilter) {
        log.info("Releasing lock for eventFilter: " + eventFilter);
        lock.release(Collections.singletonList(eventFilter), lockCollection, lockSecret);
    }
}

Screen Shot 2022-04-29 at 1 51 38 PM

The weird thing is this was working before, so not sure what I could be doing wrong. Any help will be highly appreciated.

Thanks

Potential problems with the algorithm

Hi!

Recently I decided to create a complete environment of Microservices architecture and implement some important concepts like Distributed Lock, with the goal to be a sandbox for everyone who wants to play with Microservices.

So, I created a repository simulating a Hotel Booking System.

After some researches I decided to use your lib to handle my distributed lock, but today one thread started around Distributed Lock Manager and I think your opinion will be really important for the community.

Here is the link: juliofalbo/complete-microservices-env#1

Thank you very much!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.