1 /* 2 * Copyright 2022 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.libraries.mobiledatadownload.file.openers; 17 18 import android.net.Uri; 19 import com.google.android.libraries.mobiledatadownload.file.OpenContext; 20 import com.google.android.libraries.mobiledatadownload.file.Opener; 21 import com.google.android.libraries.mobiledatadownload.file.common.FileChannelConvertible; 22 import com.google.android.libraries.mobiledatadownload.file.common.ReleasableResource; 23 import com.google.errorprone.annotations.CanIgnoreReturnValue; 24 import java.io.Closeable; 25 import java.io.IOException; 26 import java.io.RandomAccessFile; 27 import java.nio.channels.FileChannel; 28 import javax.annotation.Nullable; 29 30 /** 31 * An opener for acquiring lock files. 32 * 33 * <p>Lock files are used to separate lock acquisition from IO on the target file itself. For a 34 * target file "data.txt", an associated lock file "data.txt.lock" is created and used to control 35 * locking instead of acquiring a file lock on "data.txt" itself. This means the lock holder can 36 * perform a wider range of operations on the target file than would have been possible with a 37 * simple file lock on the target; the lock acts as an independent semaphore. 38 * 39 * <p>Note that this opener is incompatible with opaque URIs, e.g. "file:///foo.txt" is compatible 40 * whereas "memory:foo.txt" is not. 41 * 42 * <p>TODO: consider allowing client to specify lock file in order to support opaque URIs. 43 */ 44 public final class LockFileOpener implements Opener<Closeable> { 45 46 public static final String LOCK_SUFFIX = ".lock"; 47 48 private final boolean shared; 49 private final boolean readOnly; 50 private boolean isNonBlocking; 51 LockFileOpener(boolean shared, boolean readOnly)52 private LockFileOpener(boolean shared, boolean readOnly) { 53 this.shared = shared; 54 this.readOnly = readOnly; 55 } 56 57 /** 58 * Creates an instance that will acquire an exclusive lock on the file. {@link #open} will create 59 * the lock file if it doesn't already exist. 60 */ createExclusive()61 public static LockFileOpener createExclusive() { 62 return new LockFileOpener(/* shared= */ false, /* readOnly= */ false); 63 } 64 65 /** 66 * Creates an instance that will acquire a shared lock on the file (shared across processes; 67 * multiple threads in the same process exclude one another). {@link #open} won't create the lock 68 * file if it doesn't already exist (instead throwing {@code FileNotFoundException}), meaning this 69 * opener is read-only. 70 */ createReadOnlyShared()71 public static LockFileOpener createReadOnlyShared() { 72 return new LockFileOpener(/* shared= */ true, /* readOnly= */ true); 73 } 74 75 /** 76 * Creates an instance that will acquire a shared lock on the file (shared across processes; 77 * multiple threads in the same process exclude one another). {@link #open} *will* create the lock 78 * file if it doesn't already exist. 79 */ createShared()80 public static LockFileOpener createShared() { 81 return new LockFileOpener(/* shared= */ true, /* readOnly= */ false); 82 } 83 84 /** 85 * If enabled and the lock cannot be acquired immediately, {@link #open} will return {@code null} 86 * instead of waiting until the lock can be acquired. 87 */ 88 @CanIgnoreReturnValue nonBlocking(boolean isNonBlocking)89 public LockFileOpener nonBlocking(boolean isNonBlocking) { 90 this.isNonBlocking = isNonBlocking; 91 return this; 92 } 93 94 // TODO(b/131180722): consider adding option for blocking with timeout 95 96 @Override 97 @Nullable open(OpenContext openContext)98 public Closeable open(OpenContext openContext) throws IOException { 99 // Clearing fragment is necessary to open a FileChannelConvertible stream. 100 Uri lockUri = 101 openContext 102 .originalUri() 103 .buildUpon() 104 .path(openContext.encodedUri().getPath() + LOCK_SUFFIX) 105 .fragment("") 106 .build(); 107 108 try (ReleasableResource<Closeable> threadLockResource = 109 ReleasableResource.create(openThreadLock(openContext, lockUri))) { 110 if (threadLockResource.get() == null) { 111 return null; 112 } 113 114 try (ReleasableResource<Closeable> streamResource = 115 ReleasableResource.create(openStreamForLocking(openContext, lockUri)); 116 ReleasableResource<Closeable> fileLockResource = 117 ReleasableResource.create(openFileLock(openContext, streamResource.get()))) { 118 if (fileLockResource.get() == null) { 119 return null; 120 } 121 122 // The thread lock guards access to the stream and file lock so *must* be closed last, and 123 // a file lock must be closed before its underlying file so *must* be closed first. 124 Closeable threadLock = threadLockResource.release(); 125 Closeable stream = streamResource.release(); 126 Closeable fileLock = fileLockResource.release(); 127 return () -> { 128 try (Closeable last = threadLock; 129 Closeable middle = stream; 130 Closeable first = fileLock) {} 131 }; 132 } 133 } 134 } 135 136 /** 137 * Acquires (or tries to acquire) the cross-thread lock for {@code lockUri}. This is a 138 * sub-operation of {@link #open}. 139 */ 140 @Nullable openThreadLock(OpenContext openContext, Uri lockUri)141 private Closeable openThreadLock(OpenContext openContext, Uri lockUri) throws IOException { 142 if (isNonBlocking) { 143 return openContext.backend().lockScope().tryThreadLock(lockUri); 144 } else { 145 return openContext.backend().lockScope().threadLock(lockUri); 146 } 147 } 148 149 /** Opens a stream to {@code lockUri}. This is a sub-operation of {@link #open}. */ openStreamForLocking(OpenContext openContext, Uri lockUri)150 private Closeable openStreamForLocking(OpenContext openContext, Uri lockUri) throws IOException { 151 if (shared && readOnly) { 152 return openContext.backend().openForRead(lockUri); 153 } else if (shared && !readOnly) { 154 return openContext.storage().open(lockUri, RandomAccessFileOpener.createForReadWrite()); 155 } else { 156 return openContext.backend().openForWrite(lockUri); 157 } 158 } 159 160 /** 161 * Acquires (or tries to acquire) the cross-process lock for {@code stream}. Fails if the stream 162 * can't be converted to FileChannel. This is a sub-operation of {@link #open}. 163 */ 164 @Nullable openFileLock(OpenContext openContext, Closeable closeable)165 private Closeable openFileLock(OpenContext openContext, Closeable closeable) throws IOException { 166 FileChannel channel = getFileChannelFromCloseable(closeable); 167 if (isNonBlocking) { 168 return openContext.backend().lockScope().tryFileLock(channel, shared); 169 } else { 170 return openContext.backend().lockScope().fileLock(channel, shared); 171 } 172 } 173 getFileChannelFromCloseable(Closeable closeable)174 private static FileChannel getFileChannelFromCloseable(Closeable closeable) throws IOException { 175 // TODO(b/181119642): Update code so we are not casing on instanceof. 176 if (closeable instanceof FileChannelConvertible) { 177 return ((FileChannelConvertible) closeable).toFileChannel(); 178 } else if (closeable instanceof RandomAccessFile) { 179 return ((RandomAccessFile) closeable).getChannel(); 180 } else { 181 throw new IOException("Lock stream not convertible to FileChannel"); 182 } 183 } 184 } 185