netty5/buffer/src/main/java/io/netty/buffer/api/internal/ResourceSupport.java
Chris Vest 765f8989ca
Introduce alternative Buffer API (#11347)
Motivation:

In Netty 5 we wish to have a simpler, safe, future proof, and more consistent buffer API.
We developed such an API in the incubating buffer repository, and taking it through multiple rounds of review and adjustments.
This PR/commit bring the results of that work into the Netty 5 branch of the main Netty repository.

Modifications:

* `Buffer` is an interface, and all implementations are hidden behind it.
  There is no longer an inheritance hierarchy of abstract classes and implementations.
* Reference counting is gone.
  After a buffer has been allocated, calling `close` on it will deallocate it.
  It is then up to users and integrators to ensure that the life-times of buffers are managed correctly.
  This is usually not a problem as buffers tend to flow through the pipeline to be released after a terminal IO operation.
* Slice and duplicate methods are replaced with `split`.
  By removing slices, duplicate, and reference counting, there is no longer a possibility that a buffer and/or its memory can be shared and accessible through multiple routes.
  This solves the problem of data being accessed from multiple places in an uncoordinated way, and the problem of buffer memory being closed while being in use by some unsuspecting piece of code.
  Some adjustments will have to be made to other APIs, idioms, and usages, since `split` is not always a replacement for `slice` in some use cases.
* The `split` has been added which allows memory to be shared among multiple buffers, but in non-overlapping regions.
  When the memory regions don't overlap, it will not be possible for the different buffers to interfere with each other.
  An internal, and completely transparent, reference counting system ensures that the backing memory is released once the last buffer view is closed.
* A Send API has been introduced that can be used to enforce (in the type system) the transfer of buffer ownership.
  This is not expected to be used in the pipeline flow itself, but rather for other objects that wrap buffers and wish to avoid becoming "shared views" — the absence of "shared views" of memory is important for avoiding bugs in the absence of reference counting.
* A new BufferAllocator API, where the choice of implementation determines factors like on-/off-heap, pooling or not.
  How access to the different allocators will be exposed to integrators will be decided later.
  Perhaps they'll be directly accessible on the `ChannelHandlerContext`.
* The `PooledBufferAllocator` has been copied and modified to match the new allocator API.
  This includes unifying its implementation that was previously split across on-heap and off-heap.
* The `PooledBufferAllocator` implementation has also been adjusted to allocate 4 MiB chunks by default, and a few changes have been made to the implementation to make a newly created, empty allocator use significantly less heap memory.
* A `Resource` interface has been added, which defines the life-cycle methods and the `send` method.
  The `Buffer` interface extends this.
* Analogues for `ByteBufHolder` has been added in the `BufferHolder` and `BufferRef` classes.
* `ByteCursor` is added as a new way to iterate the data in buffers.
  The byte cursor API is designed to be more JIT friendly than an iterator, or the existing `ByteProcessor` interface.
* `CompositeBuffer` no longer permit the same level of access to its internal components.
  The composite buffer enforces its ownership of its components via the `Send` API, and the components can only be individually accessed with the `forEachReadable` and `forEachWritable` methods.
  This keeps the API and behavioral differences between composite and non-composite buffers to a minimum.
* Two implementations of the `Buffer` interface are provided with the API: One based on `ByteBuffer`, and one based on `sun.misc.Unsafe`.
  The `ByteBuffer` implementation is used by default.
  More implementations can be loaded from the classpath via service loading.
  The `MemorySegment` based implementation is left behind in the incubator repository.
* An extensive and highly parameterised test suite has been added, to ensure that all implementations have consistent and correct behaviour, regardless of their configuration or composition.

Result:

We have a new buffer API that is simpler, better tested, more consistent in behaviour, and safer by design, than the existing `ByteBuf` API.

The next legs of this journey will be about integrating this new API into Netty proper, and deprecate (and eventually remove) the `ByteBuf` API.

This fixes #11024, #8601, #8543, #8542, #8534, #3358, and #3306.
2021-06-28 12:06:44 +02:00

232 lines
8.4 KiB
Java

/*
* Copyright 2021 The Netty Project
*
* The Netty Project licenses this file to you 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:
*
* https://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 io.netty.buffer.api.internal;
import io.netty.buffer.api.Drop;
import io.netty.buffer.api.Owned;
import io.netty.buffer.api.Resource;
import io.netty.buffer.api.Send;
import java.util.Objects;
/**
* Internal support class for resources.
*
* @param <I> The public interface for the resource.
* @param <T> The concrete implementation of the resource.
*/
public abstract class ResourceSupport<I extends Resource<I>, T extends ResourceSupport<I, T>> implements Resource<I> {
private int acquires; // Closed if negative.
private Drop<T> drop;
private final LifecycleTracer tracer;
protected ResourceSupport(Drop<T> drop) {
this.drop = drop;
tracer = LifecycleTracer.get();
}
/**
* Encapsulation bypass for calling {@link #acquire()} on the given object.
* <p>
* Note: this {@code acquire} method does not check the type of the return value from acquire at compile time.
* The type is instead checked at runtime, and will cause a {@link ClassCastException} to be thrown if done
* incorrectly.
*
* @param obj The object we wish to acquire (increment reference count) on.
* @param <T> The type of the acquired object, given by target-typing.
* @return The acquired object.
*/
@SuppressWarnings("unchecked")
static <T> T acquire(ResourceSupport<?, ?> obj) {
return (T) obj.acquire();
}
/**
* Increment the reference count.
* <p>
* Note, this method is not thread-safe because Resources are meant to thread-confined.
*
* @return This {@link Resource} instance.
*/
protected final I acquire() {
if (acquires < 0) {
throw attachTrace(createResourceClosedException());
}
if (acquires == Integer.MAX_VALUE) {
throw new IllegalStateException("Reached maximum allowed acquires (" + Integer.MAX_VALUE + ").");
}
acquires++;
tracer.acquire(acquires);
return self();
}
protected abstract RuntimeException createResourceClosedException();
/**
* Decrement the reference count, and dispose of the resource if the last reference is closed.
* <p>
* Note, this method is not thread-safe because Resources are meant to be thread-confined.
*
* @throws IllegalStateException If this Resource has already been closed.
*/
@Override
public final void close() {
if (acquires == -1) {
throw attachTrace(new IllegalStateException("Double-free: Resource already closed and dropped."));
}
if (acquires == 0) {
tracer.drop(acquires);
drop.drop(impl());
}
acquires--;
tracer.close(acquires);
}
/**
* Send this Resource instance to another Thread, transferring the ownership to the recipient.
* This method can be used when the receiving thread is not known up front.
* <p>
* This instance immediately becomes inaccessible, and all attempts at accessing this resource will throw.
* Calling {@link #close()} will have no effect, so this method is safe to call within a try-with-resources
* statement.
*
* @throws IllegalStateException if this object has any outstanding acquires; that is, if this object has been
* {@link #acquire() acquired} more times than it has been {@link #close() closed}.
*/
@Override
public final Send<I> send() {
if (acquires < 0) {
throw attachTrace(createResourceClosedException());
}
if (!isOwned()) {
throw notSendableException();
}
var owned = tracer.send(prepareSend(), acquires);
acquires = -2; // Close without dropping. This also ignore future double-free attempts.
return new SendFromOwned<I, T>(owned, drop, getClass());
}
/**
* Attach a trace of the life-cycle of this object as suppressed exceptions to the given throwable.
*
* @param throwable The throwable to attach a life-cycle trace to.
* @param <E> The concrete exception type.
* @return The given exception, which can then be thrown.
*/
protected <E extends Throwable> E attachTrace(E throwable) {
return tracer.attachTrace(throwable);
}
/**
* Create an {@link IllegalStateException} with a custom message, tailored to this particular
* {@link Resource} instance, for when the object cannot be sent for some reason.
* @return An {@link IllegalStateException} to be thrown when this object cannot be sent.
*/
protected IllegalStateException notSendableException() {
return new IllegalStateException(
"Cannot send() a reference counted object with " + countBorrows() + " borrows: " + this + '.');
}
/**
* Encapsulation bypass to call {@link #isOwned()} on the given object.
*
* @param obj The object to query the ownership state on.
* @return {@code true} if the given object is owned, otherwise {@code false}.
*/
static boolean isOwned(ResourceSupport<?, ?> obj) {
return obj.isOwned();
}
/**
* Query if this object is in an "owned" state, which means no other references have been
* {@linkplain #acquire() acquired} to it.
*
* This would usually be the case, since there are no public methods for acquiring references to these objects.
*
* @return {@code true} if this object is in an owned state, otherwise {@code false}.
*/
protected boolean isOwned() {
return acquires == 0;
}
/**
* Encapsulation bypass to call {@link #countBorrows()} on the given object.
*
* @param obj The object to count borrows on.
* @return The number of borrows, or outstanding {@linkplain #acquire() acquires}, if any, of the given object.
*/
static int countBorrows(ResourceSupport<?, ?> obj) {
return obj.countBorrows();
}
/**
* Count the number of borrows of this object.
* Note that even if the number of borrows is {@code 0}, this object might not be {@linkplain #isOwned() owned}
* because there could be other restrictions involved in ownership.
*
* @return The number of borrows, if any, of this object.
*/
protected int countBorrows() {
return Math.max(acquires, 0);
}
@Override
public boolean isAccessible() {
return acquires >= 0;
}
/**
* Prepare this instance for ownership transfer. This method is called from {@link #send()} in the sending thread.
* This method should put this resource in a deactivated state where it is no longer accessible from the currently
* owning thread.
* In this state, the resource instance should only allow a call to {@link Owned#transferOwnership(Drop)} in the
* recipient thread.
*
* @return This resource instance in a deactivated state.
*/
protected abstract Owned<T> prepareSend();
/**
* Get access to the underlying {@link Drop} object.
* This method is unsafe because it opens the possibility of bypassing and overriding resource lifetimes.
*
* @return The {@link Drop} object used by this reference counted object.
*/
protected Drop<T> unsafeGetDrop() {
return drop;
}
/**
* Replace the current underlying {@link Drop} object with the given one.
* This method is unsafe because it opens the possibility of bypassing and overriding resource lifetimes.
*
* @param replacement The new {@link Drop} object to use instead of the current one.
*/
protected void unsafeSetDrop(Drop<T> replacement) {
drop = Objects.requireNonNull(replacement, "Replacement drop cannot be null.");
}
@SuppressWarnings("unchecked")
private I self() {
return (I) this;
}
@SuppressWarnings("unchecked")
private T impl() {
return (T) this;
}
}