• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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