jetty / jetty/jetty.project

Introduce `java.nio.ByteBuffer` abstraction

Open
#14,865 6 comments 0 reactions 0 assignees Claimed by @lorban View on GitHub
Enhancement
Dominant language
Java
Stars
4.1k
Forks
2k
Avg merge
3d 56m
Merged PRs (30d)
48

Description

### Problem description
Until now, Jetty has relied on `java.nio.ByteBuffer` for all its data manipulation: reading, writing and transporting data across layers. While this is the natural thing to do in any java application that performs non-blocking IO, this comes with some limitations:

- `java.nio.ByteBuffer` has the concept of _flipping_ that has to happen between the filling of the buffer and its flushing. While we had many debates over the year about the seriousness of this problem, the current state of Jetty is that a buffer has _modes_, and is always assumed to be in _flush_ mode, and explicitly moved to _fill_ mode when needed before reverting the mode after the operation is done.

- Lifecycle tracking: buffers are pooled because Jetty cannot afford allocating large chunks of memory and leaving them to the GC to collect, even more so with direct byte buffers that wrap non-heap memory as the GC cannot keep up with the work required to free the non-heap memory. So `RetainableByteBuffer` was introduced as a combination of `Retainable` for the lifecycle tracking, and `java.nio.ByteBuffer` still handling the data manipulation logic.

- Since `java.nio.ByteBuffer` is a final class, it cannot be extended to abstract-out similar concepts, like a collection of `java.nio.ByteBuffer` acting like a single one or a file channel being presented with the `java.nio.ByteBuffer` API, which impacts implementation: there are APIs taking a single `java.nio.ByteBuffer` or a list/array of `java.nio.ByteBuffer` and there is no easy way to make use of `FileChannel.transferTo()` to do zero-copy file-to-socket transfers. The `RetainableByteBuffer.Dynamic` with the `writeTo()` method was introduced to address the multi-buffer problem but only partially addresses it and doesn't help using `transferTo()`.

### Solution

Introduce a pair of data-manipulation interfaces: `ReadableBuffer` and `WritableBuffer` that:
- totally encapsulate `java.nio.ByteBuffer` so it is never exposed
- are read-only and write-only, **NOT** both, with an API similar to `java.nio.ByteBuffer` `get()` and `put()`
- have an easy and cheap way to switch from one to the other, similar to `java.nio.ByteBuffer.flip()` but that returns the sibling interface when used
- can flush itself (`ReadableBuffer`) or fill itself (`WritableBuffer`) via a `writeTo()` / `readFrom()` method

`WritableBuffer` only allows _writing data_ to the buffer with `put()` methods equivalent to the `java.nio.ByteBuffer` ones while `ReadableBuffer` only allows _reading data_ with `get()` methods also equivalent to the `java.nio.ByteBuffer` ones. This is comparable to the current model of only passing `java.nio.ByteBuffer` in a specific mode as it is always obvious if you can fill or flush the buffer, but this solution also make sure that the buffer is always used the intended way where it is used as those types are enforced by the compiler. `RetainableByteBuffer.Mutable` was introduced for a similar reason but since it extends from `RetainableByteBuffer`, there are places where one is used in place of the other, and since eventually `java.nio.ByteBuffer` is still exposed, the fill/flush mode problematic is still present.

There can be as many `ReadableBuffer`/`WritableBuffer` implementations as needed: backed by a single buffer, backed by a list of buffers doing either aggregation or accumulation, backed by a `FileChannel`...

Since `java.nio.ByteBuffer` is still required to perform IO, `ReadableBuffer` introduces a `writeTo(Target)` method and `WritableBuffer` a `readFrom(Fount)` method. The `Target` and `Fount` types are interfaces which can be extended at will to support optimizations:

in `WritableBuffer`:
```
interface Fount
{
/**
* @param byteBuffer the buffer to read into
*/
int read(java.nio.ByteBuffer byteBuffer) throws IOException;
}

interface ScatteringFount
{
/**
* @param byteBuffers the buffers to read into
*/
long read(java.nio.ByteBuffer[] byteBuffers) throws IOException;
}
```
and in `ReadableBuffer`:
```
interface Target
{
/**
* @param byteBuffer the buffer to be written
*/
int write(java.nio.ByteBuffer byteBuffer) throws IOException;
}

interface GatheringTarget
{
/**
* @param byteBuffers the buffers to be written
*/
long write(java.nio.ByteBuffer[] byteBuffers) throws IOException;
}

interface TransferingTarget
{
/**
* @param channel the file channel to be written
*/
long write(FileChannel channel) throws IOException;
}
```

Implementations of `ReadableBuffer` and `WritableBuffer` can then use `instanceof` to check the kind of `Target` passed if it does support a certain kind of optimization, while `Fount` and `Target` taking a single buffer act as a fall-back solution that must be supported by all implementations:

```
public class MultiReadableBuffer
{
private ByteBuffer[] buffers;

public long writeTo(Target target)
{
if (target instanceof GatheringTarget gatheringTarget)
return gatheringTarget.write(buffers);
// fallback to iterating over all buffers and writing one at a time with target.write(buffer)
}
}
```

### Scope

This covers the low-level Jetty IO. On top of the buffers, Jetty has a concept of `Content.Chunk`, `Content.Source` and `Content.Sink` that also have to be addressed for Jetty 13 but are outside the scope of this enhancement.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.