diff --git a/.github/workflows/ci-shedlock.yaml b/.github/workflows/ci-shedlock.yaml
new file mode 100644
index 0000000..cbec187
--- /dev/null
+++ b/.github/workflows/ci-shedlock.yaml
@@ -0,0 +1,47 @@
+name: YDB ShedLock CI with Maven
+
+on:
+ push:
+ paths:
+ - 'shedlock-ydb/**'
+ branches:
+ - main
+ pull_request:
+ paths:
+ - 'shedlock-ydb/**'
+
+env:
+ MAVEN_ARGS: --batch-mode --update-snapshots -Dstyle.color=always
+
+jobs:
+ build:
+ name: YDB ShedLock Lock Provider
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ java: [ '17', '21' ]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK ${{matrix.java}}
+ uses: actions/setup-java@v4
+ with:
+ java-version: ${{matrix.java}}
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Extract YDB ShedLock Lock Provider version
+ working-directory: ./shedlock-ydb
+ run: |
+ VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
+ echo "SHEDLOCK_VERSION=$VERSION" >> "$GITHUB_ENV"
+
+ - name: Download ShedLock Lock Provider dependencies
+ working-directory: ./shedlock-ydb
+ run: mvn $MAVEN_ARGS dependency:go-offline
+
+ - name: Build ShedLock Lock Provider
+ working-directory: ./shedlock-ydb
+ run: mvn $MAVEN_ARGS clean test
diff --git a/.github/workflows/publish-shedlock-ydb.yaml b/.github/workflows/publish-shedlock-ydb.yaml
new file mode 100644
index 0000000..a49604d
--- /dev/null
+++ b/.github/workflows/publish-shedlock-ydb.yaml
@@ -0,0 +1,85 @@
+name: Publish YDB ShedLock
+
+on:
+ push:
+ tags:
+ - 'shedlock-ydb/v[0-9]+.[0-9]+.[0-9]+'
+
+env:
+ MAVEN_ARGS: --batch-mode --no-transfer-progress -Dstyle.color=always
+
+jobs:
+ validate:
+ name: Validate YDB ShedLock
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Extract shedlock ydb version
+ run: |
+ cd shedlock-ydb
+ SHEDLOCK_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
+ echo "SHEDLOCK_VERSION=$SHEDLOCK_VERSION" >> "$GITHUB_ENV"
+
+ - name: Fail workflow if version is snapshot
+ if: endsWith(env.SHEDLOCK_VERSION, 'SNAPSHOT')
+ uses: actions/github-script@v6
+ with:
+ script: core.setFailed('SNAPSHOT version cannot be published')
+
+ - name: Fail workflow if version is not equal to tag name
+ if: format('shedlock-ydb/v{0}', env.SHEDLOCK_VERSION) != github.ref_name
+ uses: actions/github-script@v6
+ with:
+ script: core.setFailed('Release name must be equal to project version')
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ java-version: 17
+ distribution: 'temurin'
+ cache: 'maven'
+
+ - name: Download dependencies
+ run: |
+ cd shedlock-ydb
+ mvn $MAVEN_ARGS dependency:go-offline
+
+ - name: Build with Maven
+ run: |
+ cd shedlock-ydb
+ mvn $MAVEN_ARGS package
+
+ publish:
+ name: Publish YDB ShedLock
+ runs-on: ubuntu-latest
+ needs: validate
+
+ steps:
+ - name: Install gpg secret key
+ run: |
+ # Install gpg secret key
+ cat <(echo -e "${{ secrets.MAVEN_OSSRH_GPG_SECRET_KEY }}") | gpg --batch --import
+ # Verify gpg secret key
+ gpg --list-secret-keys --keyid-format LONG
+
+ - uses: actions/checkout@v4
+
+ - name: Set up Maven Central Repository
+ uses: actions/setup-java@v4
+ with:
+ java-version: 17
+ distribution: 'temurin'
+ cache: 'maven'
+ server-id: ossrh-s01
+ server-username: MAVEN_USERNAME
+ server-password: MAVEN_PASSWORD
+
+ - name: Publish package
+ run: |
+ cd shedlock-ydb
+ mvn $MAVEN_ARGS -Possrh-s01 -Dgpg.passphrase=${{ secrets.MAVEN_OSSRH_GPG_PASSWORD }} clean deploy
+ env:
+ MAVEN_USERNAME: ${{ secrets.MAVEN_OSSRH_USERNAME }}
+ MAVEN_PASSWORD: ${{ secrets.MAVEN_OSSRH_TOKEN }}
diff --git a/shedlock-ydb/pom.xml b/shedlock-ydb/pom.xml
new file mode 100644
index 0000000..82234b9
--- /dev/null
+++ b/shedlock-ydb/pom.xml
@@ -0,0 +1,201 @@
+
+
+ 4.0.0
+
+ tech.ydb.dialects
+ shedlock-ydb
+ 0.1.0
+
+ jar
+
+ ShedLock Service YDB
+ Lock Service YDB Spring Starter
+ https://github.com/ydb-platform/ydb-java-dialects
+
+
+
+ Kirill Kurdyukov
+ kurdyukov-kir@ydb.tech
+ YDB
+ https://ydb.tech/
+
+
+
+
+ https://github.com/ydb-platform/ydb-java-dialects
+ scm:git:https://github.com/ydb-platform/ydb-java-dialects.git
+ scm:git:https://github.com/ydb-platform/ydb-java-dialects.git
+
+
+
+ UTF-8
+
+ 17
+ 17
+ 17
+
+ 5.9.3
+ 2.17.2
+ 2.2.6
+ 2.2.2
+ 3.2.3
+ 5.15.0
+
+
+
+
+ Apache License, Version 2.0
+ https://www.apache.org/licenses/LICENSE-2.0
+
+
+
+
+
+
+ tech.ydb
+ ydb-sdk-bom
+ ${ydb.sdk.version}
+ pom
+ import
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring.boot.version}
+ import
+ pom
+
+
+
+
+
+
+ tech.ydb
+ ydb-sdk-coordination
+
+
+ net.javacrumbs.shedlock
+ shedlock-spring
+ ${shedlock-spring.version}
+ provided
+
+
+ tech.ydb.jdbc
+ ydb-jdbc-driver
+ ${ydb.jdbc.version}
+ provided
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+ provided
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ tech.ydb.test
+ ydb-junit5-support
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.5.0
+
+ 17
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.1.0
+
+
+ true
+
+
+
+
+
+
+
+
+ ossrh-s01
+
+ false
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.1.0
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.6.13
+ true
+
+ ossrh-s01
+ https://s01.oss.sonatype.org/
+ false
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/shedlock-ydb/src/main/java/tech/ydb/lock/provider/YdbCoordinationServiceLockProvider.java b/shedlock-ydb/src/main/java/tech/ydb/lock/provider/YdbCoordinationServiceLockProvider.java
new file mode 100644
index 0000000..352de67
--- /dev/null
+++ b/shedlock-ydb/src/main/java/tech/ydb/lock/provider/YdbCoordinationServiceLockProvider.java
@@ -0,0 +1,78 @@
+package tech.ydb.lock.provider;
+
+import java.sql.SQLException;
+import java.util.Optional;
+import javax.annotation.PreDestroy;
+import net.javacrumbs.shedlock.core.LockConfiguration;
+import net.javacrumbs.shedlock.core.LockProvider;
+import net.javacrumbs.shedlock.core.SimpleLock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import tech.ydb.coordination.CoordinationClient;
+import tech.ydb.coordination.SemaphoreLease;
+import tech.ydb.jdbc.YdbConnection;
+
+/**
+ * @author Kirill Kurdyukov
+ */
+public class YdbCoordinationServiceLockProvider implements LockProvider {
+ private static final Logger logger = LoggerFactory.getLogger(YdbCoordinationServiceLockProvider.class);
+ private static final String YDB_LOCK_NODE_NAME = "shared-lock-ydb";
+ private static final int ATTEMPT_CREATE_NODE = 10;
+
+ private final YdbConnection ydbConnection;
+ private final CoordinationClient coordinationClient;
+
+ public YdbCoordinationServiceLockProvider(YdbConnection ydbConnection) {
+ this.ydbConnection = ydbConnection;
+ this.coordinationClient = CoordinationClient.newClient(ydbConnection.getCtx().getGrpcTransport());
+ }
+
+ public void init() {
+ for (int i = 0; i < ATTEMPT_CREATE_NODE; i++) {
+ var status = coordinationClient.createNode(YDB_LOCK_NODE_NAME).join();
+
+ if (status.isSuccess()) {
+ return;
+ }
+
+ if (i == ATTEMPT_CREATE_NODE - 1) {
+ status.expectSuccess("Failed created coordination service node: " + YDB_LOCK_NODE_NAME);
+ }
+ }
+ }
+
+ @Override
+ public Optional lock(LockConfiguration lockConfiguration) {
+ var coordinationSession = coordinationClient.createSession(YDB_LOCK_NODE_NAME);
+
+ coordinationSession.connect().join()
+ .expectSuccess("Failed creating coordination node session");
+
+ logger.debug("Created coordination node session");
+
+ var semaphoreLease = coordinationSession.acquireEphemeralSemaphore(lockConfiguration.getName(), true,
+ lockConfiguration.getLockAtMostFor()).join();
+
+ if (semaphoreLease.isSuccess()) {
+ logger.debug("Semaphore acquired");
+
+ return Optional.of(new YdbSimpleLock(semaphoreLease.getValue()));
+ } else {
+ logger.debug("Semaphore is not acquired");
+ return Optional.empty();
+ }
+ }
+
+ private record YdbSimpleLock(SemaphoreLease semaphoreLease) implements SimpleLock {
+ @Override
+ public void unlock() {
+ semaphoreLease.release().join();
+ }
+ }
+
+ @PreDestroy
+ private void close() throws SQLException {
+ ydbConnection.close();
+ }
+}
diff --git a/shedlock-ydb/src/main/java/tech/ydb/lock/provider/YdbLockProviderConfiguration.java b/shedlock-ydb/src/main/java/tech/ydb/lock/provider/YdbLockProviderConfiguration.java
new file mode 100644
index 0000000..a3507bb
--- /dev/null
+++ b/shedlock-ydb/src/main/java/tech/ydb/lock/provider/YdbLockProviderConfiguration.java
@@ -0,0 +1,23 @@
+package tech.ydb.lock.provider;
+
+import java.sql.SQLException;
+import javax.sql.DataSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import tech.ydb.jdbc.YdbConnection;
+
+/**
+ * @author Kirill Kurdyukov
+ */
+
+@Configuration
+public class YdbLockProviderConfiguration {
+ @Bean
+ public YdbCoordinationServiceLockProvider ydbLockProvider(DataSource dataSource) throws SQLException {
+ var provider = new YdbCoordinationServiceLockProvider(dataSource.getConnection().unwrap(YdbConnection.class));
+
+ provider.init();
+
+ return provider;
+ }
+}
diff --git a/shedlock-ydb/src/main/java/tech/ydb/lock/provider/package-info.java b/shedlock-ydb/src/main/java/tech/ydb/lock/provider/package-info.java
new file mode 100644
index 0000000..6df55a8
--- /dev/null
+++ b/shedlock-ydb/src/main/java/tech/ydb/lock/provider/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * @author Kirill Kurdyukov
+ */
+@NonNullApi
+package tech.ydb.lock.provider;
+
+import org.springframework.lang.NonNullApi;
\ No newline at end of file
diff --git a/shedlock-ydb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/shedlock-ydb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..67f34c0
--- /dev/null
+++ b/shedlock-ydb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+tech.ydb.lock.provider.YdbLockProviderConfiguration
\ No newline at end of file
diff --git a/shedlock-ydb/src/test/java/tech/ydb/lock/provider/TestApp.java b/shedlock-ydb/src/test/java/tech/ydb/lock/provider/TestApp.java
new file mode 100644
index 0000000..225f8d7
--- /dev/null
+++ b/shedlock-ydb/src/test/java/tech/ydb/lock/provider/TestApp.java
@@ -0,0 +1,10 @@
+package tech.ydb.lock.provider;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Kirill Kurdyukov
+ */
+@SpringBootApplication
+public class TestApp {
+}
diff --git a/shedlock-ydb/src/test/java/tech/ydb/lock/provider/YdbLockProviderTest.java b/shedlock-ydb/src/test/java/tech/ydb/lock/provider/YdbLockProviderTest.java
new file mode 100644
index 0000000..2666768
--- /dev/null
+++ b/shedlock-ydb/src/test/java/tech/ydb/lock/provider/YdbLockProviderTest.java
@@ -0,0 +1,95 @@
+package tech.ydb.lock.provider;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import net.javacrumbs.shedlock.core.LockConfiguration;
+import net.javacrumbs.shedlock.core.SimpleLock;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import tech.ydb.test.junit5.YdbHelperExtension;
+
+/**
+ * @author Kirill Kurdyukov
+ */
+@SpringBootTest(classes = TestApp.class)
+public class YdbLockProviderTest {
+
+ @RegisterExtension
+ private static final YdbHelperExtension ydb = new YdbHelperExtension();
+
+ @DynamicPropertySource
+ private static void propertySource(DynamicPropertyRegistry registry) {
+ registry.add("spring.datasource.url", YdbLockProviderTest::jdbcUrl);
+ }
+
+ private static String jdbcUrl() {
+ StringBuilder jdbc = new StringBuilder("jdbc:ydb:")
+ .append(ydb.useTls() ? "grpcs://" : "grpc://")
+ .append(ydb.endpoint())
+ .append(ydb.database());
+
+ if (ydb.authToken() != null) {
+ jdbc.append("?").append("token=").append(ydb.authToken());
+ }
+
+ return jdbc.toString();
+ }
+
+ @Autowired
+ private YdbCoordinationServiceLockProvider lockProvider;
+
+ @Test
+ public void integrationTest() throws ExecutionException, InterruptedException {
+ var executorServer = Executors.newFixedThreadPool(10);
+ var atomicInt = new AtomicInteger();
+ var locked = new AtomicBoolean();
+ var futures = new ArrayList>();
+
+ for (int i = 0; i < 100; i++) {
+ final var ii = i;
+ futures.add(executorServer.submit(() -> {
+ Optional optinal = Optional.empty();
+
+ while (optinal.isEmpty()) {
+ optinal = lockProvider.lock(
+ new LockConfiguration(Instant.now(), "semaphore", Duration.ZERO, Duration.ZERO));
+
+ optinal.ifPresent(simpleLock -> {
+ if (locked.get()) {
+ throw new RuntimeException();
+ }
+ locked.set(true);
+
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ atomicInt.addAndGet(ii);
+ locked.set(false);
+ simpleLock.unlock();
+ });
+ }
+ }));
+ }
+
+ for (Future> future : futures) {
+ future.get();
+ }
+
+ Assertions.assertEquals(4950, atomicInt.get());
+ }
+}
diff --git a/shedlock-ydb/src/test/resources/application.properties b/shedlock-ydb/src/test/resources/application.properties
new file mode 100644
index 0000000..09763b8
--- /dev/null
+++ b/shedlock-ydb/src/test/resources/application.properties
@@ -0,0 +1 @@
+spring.datasource.driver-class-name=tech.ydb.jdbc.YdbDriver
\ No newline at end of file
diff --git a/spring-data-dialect/src/main/java/tech/ydb/data/core/convert/YdbMappingJdbcConverter.java b/spring-data-dialect/src/main/java/tech/ydb/data/core/convert/YdbMappingJdbcConverter.java
index 55f14a3..d0348f8 100644
--- a/spring-data-dialect/src/main/java/tech/ydb/data/core/convert/YdbMappingJdbcConverter.java
+++ b/spring-data-dialect/src/main/java/tech/ydb/data/core/convert/YdbMappingJdbcConverter.java
@@ -27,6 +27,7 @@ public SQLType getTargetSqlType(RelationalPersistentProperty property) {
if (property.isAnnotationPresent(YdbType.class)) {
return new YQLType(property.getRequiredAnnotation(YdbType.class).value());
}
+
return YdbJdbcUtil.targetSqlTypeFor(getColumnType(property));
}
diff --git a/spring-data-dialect/src/main/java/tech/ydb/data/core/package-info.java b/spring-data-dialect/src/main/java/tech/ydb/data/core/package-info.java
new file mode 100644
index 0000000..26f310c
--- /dev/null
+++ b/spring-data-dialect/src/main/java/tech/ydb/data/core/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * @author Kirill Kurdyukov
+ */
+package tech.ydb.data.core;
\ No newline at end of file