All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fluxtion.agrona.concurrent.RecordBuffer Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2014-2024 Real Logic Limited.
 *
 * 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
 *
 * 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 com.fluxtion.agrona.concurrent;

import java.util.concurrent.locks.LockSupport;

import static java.util.concurrent.TimeUnit.MICROSECONDS;
import static com.fluxtion.agrona.BitUtil.SIZE_OF_INT;

/**
 * A record buffer is an off-heap buffer with a series of records
 * and a header that can be written to or read from multiple threads.
 * 

* The buffer has a position field at the start, then a variable * sized header, then a series of records. Each record has a status * tag and a key. *

* NB: it's possible for an element to be overwritten as you're reading * Out of the buffer, take care. * *

 *  +----------------------------+
 *  |           Header           |
 *  |                           ...
 *  +----------------------------+
 *  |        Position Field      |
 *  +----------------------------+
 *  |        Status Field 0      |
 *  +----------------------------+
 *  |         Key Field 0        |
 *  +----------------------------+
 *  |           Record 0         |
 *  |                           ...
 *  +----------------------------+
 *  |        Status Field 1      |
 *  +----------------------------+
 *  |         Key Field 1        |
 *  +----------------------------+
 *  |           Record 1         |
 *  |                           ...
 *  +----------------------------+
 *  |                           ...
 * 
*/ // TODO: consider optimising this class by indexing on the key, rather than just linear scanning public class RecordBuffer { /** * Special offset indiciating that the record was not claimed. */ public static final int DID_NOT_CLAIM_RECORD = -1; private static final int UNUSED = 0; private static final int PENDING = 1; private static final int COMMITTED = 2; private static final int SIZE_OF_POSITION_FIELD = SIZE_OF_INT; /** * Status field is an int rather than a byte so that it can be CAS'd. */ private static final int SIZE_OF_STATUS_FIELD = SIZE_OF_INT; private static final int SIZE_OF_KEY_FIELD = SIZE_OF_INT; private static final int SIZE_OF_RECORD_FRAME = SIZE_OF_STATUS_FIELD + SIZE_OF_KEY_FIELD; private static final long PAUSE_TIME_NS = MICROSECONDS.toNanos(1000); private final AtomicBuffer buffer; private final int positionFieldOffset; private final int endOfPositionField; private final int slotSize; /** * Callback interface for reading elements out of the buffer. * * @see RecordBuffer#forEach(RecordHandler) */ public interface RecordHandler { /** * Called once for each committed record in the buffer. * * @param key the key for the record in question. * @param offset the offset within the buffer that the record starts at. */ void onRecord(int key, int offset); } /** * Interface for safely writing to the buffer. * * @see RecordBuffer#withRecord(int, RecordWriter) */ public interface RecordWriter { /** * Write an updated record within this callback. * * @param offset the offset within the buffer that has been claimed. */ void writeRecord(int offset); } /** * @param buffer to wrap. * @param headerSize in bytes. * @param recordSize in bytes. */ public RecordBuffer(final AtomicBuffer buffer, final int headerSize, final int recordSize) { this.buffer = buffer; this.positionFieldOffset = headerSize; this.endOfPositionField = headerSize + SIZE_OF_POSITION_FIELD; this.slotSize = recordSize + SIZE_OF_RECORD_FRAME; } /** * Initialise the buffer if you're the first thread to begin writing. */ public void initialise() { movePosition(endOfPositionField); } /** * Check if the buffer has been initialised. * * @return true if the buffer has been initialised. */ public boolean isInitialised() { return position() != 0; } /** * Read each record out of the buffer in turn. * * @param handler the handler is called once for each record in the buffer. */ public void forEach(final RecordHandler handler) { int offset = endOfPositionField; final int position = position(); while (offset < position) { if (statusVolatile(offset) == COMMITTED) { final int key = key(offset); handler.onRecord(key, offset + SIZE_OF_RECORD_FRAME); } offset += slotSize; } } /** * Search for the first record with the specified key. * * @param key the key to search for. * @return the offset of the start of the record within the buffer or {@code DID_NOT_CLAIM_RECORD} if no record * with the specified key. */ public int get(final int key) { int offset = endOfPositionField; final int position = position(); while (offset < position) { if (statusVolatile(offset) == COMMITTED && key == key(offset)) { return offset + SIZE_OF_RECORD_FRAME; } offset += slotSize; } return DID_NOT_CLAIM_RECORD; } /** * High level and safe way of writing a record to the buffer. * * @param key the key to associate the record with. * @param writer the callback which is passed the record to write. * @return whether the write operation succeeded or not. */ public boolean withRecord(final int key, final RecordWriter writer) { final int claimedOffset = claimRecord(key); if (claimedOffset == DID_NOT_CLAIM_RECORD) { return false; } try { writer.writeRecord(claimedOffset); } finally { commit(claimedOffset); } return true; } /** * Claim a record in the buffer. Each record has a unique key. * * @param key the key to claim the record with. * @return the offset at which record was claimed or {@code DID_NOT_CLAIM_RECORD} if the claim failed. * @see RecordBuffer#commit(int) */ public int claimRecord(final int key) { int offset = endOfPositionField; while (offset < position()) { if (key == key(offset)) { // If someone else is writing something with the same key then abort if (statusVolatile(offset) == PENDING) { return DID_NOT_CLAIM_RECORD; } else // state == COMMITTED { compareAndSetStatus(offset, COMMITTED, PENDING); return offset + SIZE_OF_RECORD_FRAME; } } offset += slotSize; } if ((offset + slotSize) > buffer.capacity()) { return DID_NOT_CLAIM_RECORD; } final int claimOffset = movePosition(slotSize); compareAndSetStatus(claimOffset, UNUSED, PENDING); key(claimOffset, key); return claimOffset + SIZE_OF_RECORD_FRAME; } /** * Commit a claimed record into the buffer. * * @param claimedOffset the offset of the record to commit. * @see RecordBuffer#claimRecord(int) */ public void commit(final int claimedOffset) { compareAndSetStatus(claimedOffset - SIZE_OF_RECORD_FRAME, PENDING, COMMITTED); } private int statusVolatile(final int offset) { return buffer.getIntVolatile(offset); } private void compareAndSetStatus(final int offset, final int oldStatus, final int newStatus) { while (!buffer.compareAndSetInt(offset, oldStatus, newStatus)) { LockSupport.parkNanos(PAUSE_TIME_NS); } } private int key(final int offset) { return buffer.getInt(offset + SIZE_OF_STATUS_FIELD); } private void key(final int offset, final int key) { buffer.putInt(offset + SIZE_OF_STATUS_FIELD, key); } private int position() { return buffer.getIntVolatile(positionFieldOffset); } private int movePosition(final int delta) { return buffer.getAndAddInt(positionFieldOffset, delta); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy