blob: c608ec2fa4dea21af5c73864f570856619f2bf36 [file] [log] [blame]
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.libraries.mobiledatadownload.file.openers;
import android.net.Uri;
import com.google.android.libraries.mobiledatadownload.file.OpenContext;
import com.google.android.libraries.mobiledatadownload.file.Opener;
import com.google.android.libraries.mobiledatadownload.file.common.FileChannelConvertible;
import com.google.android.libraries.mobiledatadownload.file.common.ReleasableResource;
import java.io.Closeable;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import javax.annotation.Nullable;
/**
* An opener for acquiring lock files.
*
* <p>Lock files are used to separate lock acquisition from IO on the target file itself. For a
* target file "data.txt", an associated lock file "data.txt.lock" is created and used to control
* locking instead of acquiring a file lock on "data.txt" itself. This means the lock holder can
* perform a wider range of operations on the target file than would have been possible with a
* simple file lock on the target; the lock acts as an independent semaphore.
*
* <p>Note that this opener is incompatible with opaque URIs, e.g. "file:///foo.txt" is compatible
* whereas "memory:foo.txt" is not.
*
* <p>TODO: consider allowing client to specify lock file in order to support opaque URIs.
*/
public final class LockFileOpener implements Opener<Closeable> {
private static final String LOCK_SUFFIX = ".lock";
private final boolean shared;
private final boolean readOnly;
private boolean isNonBlocking;
private LockFileOpener(boolean shared, boolean readOnly) {
this.shared = shared;
this.readOnly = readOnly;
}
/**
* Creates an instance that will acquire an exclusive lock on the file. {@link #open} will create
* the lock file if it doesn't already exist.
*/
public static LockFileOpener createExclusive() {
return new LockFileOpener(/* shared= */ false, /* readOnly= */ false);
}
/**
* Creates an instance that will acquire a shared lock on the file (shared across processes;
* multiple threads in the same process exclude one another). {@link #open} won't create the lock
* file if it doesn't already exist (instead throwing {@code FileNotFoundException}), meaning this
* opener is read-only.
*/
public static LockFileOpener createReadOnlyShared() {
return new LockFileOpener(/* shared= */ true, /* readOnly= */ true);
}
/**
* Creates an instance that will acquire a shared lock on the file (shared across processes;
* multiple threads in the same process exclude one another). {@link #open} *will* create the lock
* file if it doesn't already exist.
*/
public static LockFileOpener createShared() {
return new LockFileOpener(/* shared= */ true, /* readOnly= */ false);
}
/**
* If enabled and the lock cannot be acquired immediately, {@link #open} will return {@code null}
* instead of waiting until the lock can be acquired.
*/
public LockFileOpener nonBlocking(boolean isNonBlocking) {
this.isNonBlocking = isNonBlocking;
return this;
}
// TODO(b/131180722): consider adding option for blocking with timeout
@Override
@Nullable
public Closeable open(OpenContext openContext) throws IOException {
// Clearing fragment is necessary to open a FileChannelConvertible stream.
Uri lockUri =
openContext
.originalUri()
.buildUpon()
.path(openContext.encodedUri().getPath() + LOCK_SUFFIX)
.fragment("")
.build();
try (ReleasableResource<Closeable> threadLockResource =
ReleasableResource.create(openThreadLock(openContext, lockUri))) {
if (threadLockResource.get() == null) {
return null;
}
try (ReleasableResource<Closeable> streamResource =
ReleasableResource.create(openStreamForLocking(openContext, lockUri));
ReleasableResource<Closeable> fileLockResource =
ReleasableResource.create(openFileLock(openContext, streamResource.get()))) {
if (fileLockResource.get() == null) {
return null;
}
// The thread lock guards access to the stream and file lock so *must* be closed last, and
// a file lock must be closed before its underlying file so *must* be closed first.
Closeable threadLock = threadLockResource.release();
Closeable stream = streamResource.release();
Closeable fileLock = fileLockResource.release();
return () -> {
try (Closeable last = threadLock;
Closeable middle = stream;
Closeable first = fileLock) {}
};
}
}
}
/**
* Acquires (or tries to acquire) the cross-thread lock for {@code lockUri}. This is a
* sub-operation of {@link #open}.
*/
@Nullable
private Closeable openThreadLock(OpenContext openContext, Uri lockUri) throws IOException {
if (isNonBlocking) {
return openContext.backend().lockScope().tryThreadLock(lockUri);
} else {
return openContext.backend().lockScope().threadLock(lockUri);
}
}
/** Opens a stream to {@code lockUri}. This is a sub-operation of {@link #open}. */
private Closeable openStreamForLocking(OpenContext openContext, Uri lockUri) throws IOException {
if (shared && readOnly) {
return openContext.backend().openForRead(lockUri);
} else if (shared && !readOnly) {
return openContext.storage().open(lockUri, RandomAccessFileOpener.createForReadWrite());
} else {
return openContext.backend().openForWrite(lockUri);
}
}
/**
* Acquires (or tries to acquire) the cross-process lock for {@code stream}. Fails if the stream
* can't be converted to FileChannel. This is a sub-operation of {@link #open}.
*/
@Nullable
private Closeable openFileLock(OpenContext openContext, Closeable closeable) throws IOException {
FileChannel channel = getFileChannelFromCloseable(closeable);
if (isNonBlocking) {
return openContext.backend().lockScope().tryFileLock(channel, shared);
} else {
return openContext.backend().lockScope().fileLock(channel, shared);
}
}
private static FileChannel getFileChannelFromCloseable(Closeable closeable) throws IOException {
// TODO(b/181119642): Update code so we are not casing on instanceof.
if (closeable instanceof FileChannelConvertible) {
return ((FileChannelConvertible) closeable).toFileChannel();
} else if (closeable instanceof RandomAccessFile) {
return ((RandomAccessFile) closeable).getChannel();
} else {
throw new IOException("Lock stream not convertible to FileChannel");
}
}
}