Repository: eclipse-vertx/vert.x

Problem statement:
filesystem api: expose file locking operations

The `lock` and `tryLock` methods of `AsynchronousFileChannel` should be made available for `AsyncFile` wrappers, so one can perform proper file locking operations.




Hints:
Hi @panchmp , @vietj  is this still open for the grab ?
yes it is.

On Fri, Apr 2, 2021 at 5:10 PM Shay Dratler ***@***.***>
wrote:

> Hi @panchmp <https://github.com/panchmp> , @vietj
> <https://github.com/vietj> is this still open for the grab ?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/eclipse-vertx/vert.x/issues/3856#issuecomment-812571419>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AABXDCWKRQR4G7AKHPJYTBDTGXM7XANCNFSM4ZFNIWIA>
> .
>

Hi @vietj  @tsegismont is this still available? 
Yes

Patch:
diff --git a/src/main/java/io/vertx/core/file/AsyncFile.java b/src/main/java/io/vertx/core/file/AsyncFile.java
--- a/src/main/java/io/vertx/core/file/AsyncFile.java
+++ b/src/main/java/io/vertx/core/file/AsyncFile.java
@@ -12,6 +12,7 @@
 package io.vertx.core.file;
 
 import io.vertx.codegen.annotations.Fluent;
+import io.vertx.codegen.annotations.Nullable;
 import io.vertx.codegen.annotations.VertxGen;
 import io.vertx.core.AsyncResult;
 import io.vertx.core.Future;
@@ -203,4 +204,48 @@ default void size(Handler<AsyncResult<Long>> handler) {
    * @return the size of the file
    */
   Future<Long> size();
+
+  /**
+   * Try to acquire a non-shared lock on the entire file.
+   *
+   * @return the lock if it can be acquired immediately, otherwise {@code null}
+   */
+  @Nullable AsyncFileLock tryLock();
+
+  /**
+   * Try to acquire a lock on a portion of this file.
+   *
+   * @param position where the region starts
+   * @param size the size of the region
+   * @param shared whether the lock should be shared
+   * @return the lock if it can be acquired immediately, otherwise {@code null}
+   */
+  @Nullable AsyncFileLock tryLock(long position, long size, boolean shared);
+
+  /**
+   * Acquire a non-shared lock on the entire file.
+   *
+   * @return a future indicating the completion of this operation
+   */
+  Future<AsyncFileLock> lock();
+
+  /**
+   * Like {@link #lock()} but the {@code handler} will be called when the operation is complete or if an error occurs.
+   */
+  void lock(Handler<AsyncResult<AsyncFileLock>> handler);
+
+  /**
+   * Acquire a lock on a portion of this file.
+   *
+   * @param position where the region starts
+   * @param size the size of the region
+   * @param shared whether the lock should be shared
+   * @return a future indicating the completion of this operation
+   */
+  Future<AsyncFileLock> lock(long position, long size, boolean shared);
+
+  /**
+   * Like {@link #lock(long, long, boolean)} but the {@code handler} will be called when the operation is complete or if an error occurs.
+   */
+  void lock(long position, long size, boolean shared, Handler<AsyncResult<AsyncFileLock>> handler);
 }
diff --git a/src/main/java/io/vertx/core/file/AsyncFileLock.java b/src/main/java/io/vertx/core/file/AsyncFileLock.java
new file mode 100644
--- /dev/null
+++ b/src/main/java/io/vertx/core/file/AsyncFileLock.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2011-2022 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ */
+
+package io.vertx.core.file;
+
+import io.vertx.codegen.annotations.VertxGen;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+
+/**
+ * A lock on a region of an {@link AsyncFile}.
+ */
+@VertxGen
+public interface AsyncFileLock {
+
+  /**
+   * @return the position of the first byte of the locked region
+   */
+  long position();
+
+  /**
+   * @return the size in bytes of the locked region
+   */
+  long size();
+
+  /**
+   * @return {@code true} if this lock is shared, otherwise {@code false}
+   */
+  boolean isShared();
+
+  /**
+   * @return {@code true} if this lock overlaps with the range described by {@code position} and {@code size}, otherwise {@code false}
+   */
+  boolean overlaps(long position, long size);
+
+  /**
+   * Like {@link #isValid()} but blocking.
+   *
+   * @throws FileSystemException if an error occurs
+   */
+  boolean isValidBlocking();
+
+  /**
+   * A lock remains valid until it is released or the file corresponding {@link AsyncFile} is closed.
+   */
+  Future<Boolean> isValid();
+
+  /**
+   * Like {@link #isValid()} but the {@code handler} will be called when the operation completes or if an error occurs.
+   */
+  void isValid(Handler<AsyncResult<Boolean>> handler);
+
+  /**
+   * Like {@link #release()} but blocking.
+   *
+   * @throws FileSystemException if an error occurs
+   */
+  void releaseBlocking();
+
+  /**
+   * Releases this lock;
+   */
+  Future<Void> release();
+
+  /**
+   * Like {@link #release()} but the {@code handler} will be called when the operation completes or if an error occurs.
+   */
+  void release(Handler<AsyncResult<Void>> handler);
+}
diff --git a/src/main/java/io/vertx/core/file/impl/AsyncFileImpl.java b/src/main/java/io/vertx/core/file/impl/AsyncFileImpl.java
--- a/src/main/java/io/vertx/core/file/impl/AsyncFileImpl.java
+++ b/src/main/java/io/vertx/core/file/impl/AsyncFileImpl.java
@@ -18,11 +18,13 @@
 import io.vertx.core.Promise;
 import io.vertx.core.buffer.Buffer;
 import io.vertx.core.file.AsyncFile;
+import io.vertx.core.file.AsyncFileLock;
 import io.vertx.core.file.FileSystemException;
 import io.vertx.core.file.OpenOptions;
 import io.vertx.core.impl.Arguments;
 import io.vertx.core.impl.ContextInternal;
 import io.vertx.core.impl.VertxInternal;
+import io.vertx.core.impl.future.PromiseInternal;
 import io.vertx.core.impl.logging.Logger;
 import io.vertx.core.impl.logging.LoggerFactory;
 import io.vertx.core.streams.impl.InboundBuffer;
@@ -30,6 +32,8 @@
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.CompletionHandler;
+import java.nio.channels.FileLock;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -590,4 +594,69 @@ public Future<Long> size() {
       prom.complete(sizeBlocking());
     });
   }
+
+  @Override
+  public AsyncFileLock tryLock() {
+    try {
+      return new AsyncFileLockImpl(vertx, ch.tryLock());
+    } catch (IOException e) {
+      throw new FileSystemException(e);
+    }
+  }
+
+  @Override
+  public AsyncFileLock tryLock(long position, long size, boolean shared) {
+    try {
+      return new AsyncFileLockImpl(vertx, ch.tryLock(position, size, shared));
+    } catch (IOException e) {
+      throw new FileSystemException(e);
+    }
+  }
+
+  @Override
+  public Future<AsyncFileLock> lock() {
+    return lock(0, Long.MAX_VALUE, false);
+  }
+
+  @Override
+  public void lock(Handler<AsyncResult<AsyncFileLock>> handler) {
+    Future<AsyncFileLock> future = lock();
+    if (handler != null) {
+      future.onComplete(handler);
+    }
+  }
+
+  private static CompletionHandler<FileLock, PromiseInternal<AsyncFileLock>> LOCK_COMPLETION = new CompletionHandler<FileLock, PromiseInternal<AsyncFileLock>>() {
+    @Override
+    public void completed(FileLock result, PromiseInternal<AsyncFileLock> p) {
+      p.complete(new AsyncFileLockImpl(p.context().owner(), result));
+    }
+
+    @Override
+    public void failed(Throwable t, PromiseInternal<AsyncFileLock> p) {
+      p.fail(new FileSystemException(t));
+    }
+  };
+
+  @Override
+  public Future<AsyncFileLock> lock(long position, long size, boolean shared) {
+    PromiseInternal<AsyncFileLock> promise = vertx.promise();
+    vertx.executeBlockingInternal(prom -> {
+      ch.lock(position, size, shared, promise, LOCK_COMPLETION);
+    }, ar -> {
+      if (ar.failed()) {
+        // Happens only if ch.lock throws a RuntimeException
+        promise.fail(new FileSystemException(ar.cause()));
+      }
+    });
+    return promise.future();
+  }
+
+  @Override
+  public void lock(long position, long size, boolean shared, Handler<AsyncResult<AsyncFileLock>> handler) {
+    Future<AsyncFileLock> future = lock(position, size, shared);
+    if (handler != null) {
+      future.onComplete(handler);
+    }
+  }
 }
diff --git a/src/main/java/io/vertx/core/file/impl/AsyncFileLockImpl.java b/src/main/java/io/vertx/core/file/impl/AsyncFileLockImpl.java
new file mode 100644
--- /dev/null
+++ b/src/main/java/io/vertx/core/file/impl/AsyncFileLockImpl.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2011-2022 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ */
+
+package io.vertx.core.file.impl;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.file.AsyncFileLock;
+import io.vertx.core.file.FileSystemException;
+import io.vertx.core.impl.VertxInternal;
+
+import java.io.IOException;
+import java.nio.channels.FileLock;
+import java.util.Objects;
+
+public class AsyncFileLockImpl implements AsyncFileLock {
+
+  private final VertxInternal vertx;
+  private final FileLock fileLock;
+
+  public AsyncFileLockImpl(VertxInternal vertx, FileLock fileLock) {
+    this.vertx = Objects.requireNonNull(vertx, "vertx is null");
+    this.fileLock = Objects.requireNonNull(fileLock, "fileLock is null");
+  }
+
+  @Override
+  public long position() {
+    return fileLock.position();
+  }
+
+  @Override
+  public long size() {
+    return fileLock.size();
+  }
+
+  @Override
+  public boolean isShared() {
+    return fileLock.isShared();
+  }
+
+  @Override
+  public boolean overlaps(long position, long size) {
+    return fileLock.overlaps(position, size);
+  }
+
+  @Override
+  public boolean isValidBlocking() {
+    return fileLock.isValid();
+  }
+
+  @Override
+  public Future<Boolean> isValid() {
+    return vertx.getOrCreateContext().executeBlockingInternal(prom -> {
+      prom.complete(isValidBlocking());
+    });
+  }
+
+  @Override
+  public void isValid(Handler<AsyncResult<Boolean>> handler) {
+    Future<Boolean> future = isValid();
+    if (handler != null) {
+      future.onComplete(handler);
+    }
+  }
+
+  @Override
+  public void releaseBlocking() {
+    try {
+      fileLock.release();
+    } catch (IOException e) {
+      throw new FileSystemException(e);
+    }
+  }
+
+  @Override
+  public Future<Void> release() {
+    return vertx.getOrCreateContext().executeBlockingInternal(prom -> {
+      try {
+        fileLock.release();
+        prom.complete();
+      } catch (IOException e) {
+        prom.fail(new FileSystemException(e));
+      }
+    });
+  }
+
+  @Override
+  public void release(Handler<AsyncResult<Void>> handler) {
+    Future<Void> future = release();
+    if (handler != null) {
+      future.onComplete(handler);
+    }
+  }
+}


Test patch:
diff --git a/src/test/java/io/vertx/core/file/FileSystemTest.java b/src/test/java/io/vertx/core/file/FileSystemTest.java
--- a/src/test/java/io/vertx/core/file/FileSystemTest.java
+++ b/src/test/java/io/vertx/core/file/FileSystemTest.java
@@ -31,6 +31,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.channels.OverlappingFileLockException;
 import java.nio.file.*;
 import java.nio.file.attribute.*;
 import java.util.EnumSet;
@@ -2336,4 +2337,34 @@ public void testFileSizeBlocking() throws Exception {
     file.close(onSuccess(v -> testComplete()));
     await();
   }
+
+  @Test
+  public void testFileLocking() throws Exception {
+    Assume.assumeFalse(Utils.isWindows());
+    String path = tmpFile(".lock").getAbsolutePath();
+    FileSystem fs = vertx.fileSystem();
+    fs.writeFileBlocking(path, Buffer.buffer("HelloLocks"));
+
+    AsyncFile file1 = fs.openBlocking(path, new OpenOptions());
+    AsyncFile file2 = fs.openBlocking(path, new OpenOptions());
+
+    file1.lock(0, "Hello".length(), false, onSuccess(lock1 -> {
+      file2.lock(onFailure(t -> {
+        assertTrue(t instanceof FileSystemException);
+        assertTrue(t.getCause() instanceof OverlappingFileLockException);
+        file2.lock("Hello".length(), "Locks".length(), false, onSuccess(lock2 -> {
+          lock1.release(onSuccess(r1 -> {
+            lock2.release(onSuccess(r2 -> {
+              testComplete();
+            }));
+          }));
+        }));
+      }));
+    }));
+
+    await();
+
+    file1.close();
+    file2.close();
+  }
 }