/* * Copyright 2017 the original author or authors. * * 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 ratpack.exec.util; import ratpack.exec.Promise; import ratpack.exec.util.internal.DefaultReadWriteAccess; import java.time.Duration; import java.util.concurrent.locks.ReadWriteLock; /** * Provides read/write serialization, analogous to {@link ReadWriteLock}. * <p> * Can be used whenever a “resource” has safe concurrent usages and mutually exclusive usages, * such as updating a file. * <p> * The {@link #read(Promise)} and {@link #write(Promise)} methods decorate promises with serialization. * Read serialized promises may execute concurrently with other read serialized promises, * but not with write serialized promises. * Write serialized promises may not execute concurrently with read or write serialized promises. * <p> * Access is generally fair. * That is, access is granted in the order that promises execute (n.b. not in the order they are decorated). * <p> * Access is not reentrant. * Deadlocks are not detected or prevented. * * <pre class="java">{@code * import com.google.common.io.Files; * import io.netty.buffer.ByteBufAllocator; * import ratpack.exec.Promise; * import ratpack.exec.util.ParallelBatch; * import ratpack.exec.util.ReadWriteAccess; * import ratpack.file.FileIo; * import ratpack.test.embed.EmbeddedApp; * import ratpack.test.embed.EphemeralBaseDir; * import ratpack.test.exec.ExecHarness; * * import java.nio.charset.Charset; * import java.nio.file.Path; * import java.util.ArrayList; * import java.util.Collections; * import java.util.List; * import java.time.Duration; * * import static java.nio.file.StandardOpenOption.*; * import static org.junit.Assert.assertEquals; * * public class Example { * * public static void main(String... args) throws Exception { * EphemeralBaseDir.tmpDir().use(baseDir -> { * ReadWriteAccess access = ReadWriteAccess.create(Duration.ofSeconds(5)); * Path f = baseDir.write("f", "foo"); * * EmbeddedApp.of(a -> a * .serverConfig(c -> c.baseDir(baseDir.getRoot())) * .handlers(c -> { * ByteBufAllocator allocator = c.getRegistry().get(ByteBufAllocator.class); * * c.path(ctx -> * ctx.byMethod(m -> m * .get(() -> * FileIo.read(FileIo.open(f, READ, CREATE), allocator, 8192) * .apply(access::read) * .map(b -> b.toString(Charset.defaultCharset())) * .then(ctx::render) * ) * .post(() -> * FileIo.write(ctx.getRequest().getBodyStream(), FileIo.open(f, WRITE, CREATE, TRUNCATE_EXISTING)) * .apply(access::write) * .then(written -> ctx.render(written.toString())) * ) * ) * ); * }) * ).test(httpClient -> { * * // Create a bunch of reads and writes * List<Promise<String>> requests = new ArrayList<>(); * for (int i = 0; i < 200; ++i) { * requests.add(Promise.sync(httpClient::getText)); * } * for (int i = 0; i < 200; ++i) { * requests.add(Promise.sync(() -> * httpClient.request(r -> r * .post().getBody().text("foo") * ).getBody().getText() * )); * } * * // Interleave * Collections.shuffle(requests); * * // Execute them in parallel * List<String> results = ExecHarness.yieldSingle(r -> * ParallelBatch.of(requests).yield() * ).getValueOrThrow(); * * assertEquals("foo", Files.toString(f.toFile(), Charset.defaultCharset())); * assertEquals(400, results.size()); * }); * }); * } * } * * }</pre> * * @since 1.5 */ public interface ReadWriteAccess { /** * Create a new read/write access object with the given default timeout. * * @param defaultTimeout the default maximum amount of time to wait for access (must not be negative, 0 == infinite) * @return a new read/write access object */ static ReadWriteAccess create(Duration defaultTimeout) { return new DefaultReadWriteAccess(defaultTimeout); } /** * The default timeout value. * * @return the default timeout value */ Duration getDefaultTimeout(); /** * Decorates the given promise with read serialization. * <p> * Read serialized promises may execute concurrently with other read serialized promises, * but not with write serialized promises. * <p> * If access is not granted within the default timeout, the promise will wail with {@link TimeoutException}. * * @param promise the promise to decorate * @param <T> the type of promised value * @return a decorated promise */ <T> Promise<T> read(Promise<T> promise); /** * Decorates the given promise with read serialization and the given timeout. * <p> * Read serialized promises may execute concurrently with other read serialized promises, * but not with write serialized promises. * <p> * If access is not granted within the given timeout, the promise will wail with {@link TimeoutException}. * * @param promise the promise to decorate * @param timeout the maximum amount of time to wait for access (must not be negative, 0 == infinite) * @param <T> the type of promised value * @return a decorated promise */ <T> Promise<T> read(Promise<T> promise, Duration timeout); /** * Decorates the given promise with write serialization. * <p> * Write serialized promises may not execute concurrently with read or write serialized promises. * <p> * If access is not granted within the default timeout, the promise will wail with {@link TimeoutException}. * * @param promise the promise to decorate * @param <T> the type of promised value * @return a decorated promise */ <T> Promise<T> write(Promise<T> promise); /** * Decorates the given promise with write serialization. * <p> * Write serialized promises may not execute concurrently with read or write serialized promises. * <p> * If access is not granted within the given timeout, the promise will wail with {@link TimeoutException}. * * @param promise the promise to decorate * @param timeout the maximum amount of time to wait for access (must not be negative, 0 == infinite) * @param <T> the type of promised value * @return a decorated promise */ <T> Promise<T> write(Promise<T> promise, Duration timeout); /** * Thrown if access could not be acquired within the given timeout value. */ class TimeoutException extends RuntimeException { public TimeoutException(String message) { super(message); } } }