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

com.hazelcast.ringbuffer.impl.RingbufferContainer Maven / Gradle / Ivy

There is a newer version: 5.5.0
Show newest version
/*
 * Copyright (c) 2008-2023, Hazelcast, Inc. All Rights Reserved.
 *
 * 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 com.hazelcast.ringbuffer.impl;

import com.hazelcast.config.InMemoryFormat;
import com.hazelcast.config.RingbufferConfig;
import com.hazelcast.core.HazelcastException;
import com.hazelcast.internal.nio.IOUtil;
import com.hazelcast.nio.ObjectDataInput;
import com.hazelcast.nio.ObjectDataOutput;
import com.hazelcast.internal.serialization.Data;
import com.hazelcast.nio.serialization.HazelcastSerializationException;
import com.hazelcast.nio.serialization.IdentifiedDataSerializable;
import com.hazelcast.ringbuffer.StaleSequenceException;
import com.hazelcast.spi.impl.NodeEngine;
import com.hazelcast.spi.impl.operationservice.Notifier;
import com.hazelcast.internal.services.ObjectNamespace;
import com.hazelcast.spi.impl.operationservice.WaitNotifyKey;
import com.hazelcast.internal.serialization.SerializationService;

import java.io.IOException;

import static com.hazelcast.config.InMemoryFormat.BINARY;
import static com.hazelcast.config.InMemoryFormat.OBJECT;
import static com.hazelcast.config.InMemoryFormat.values;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * The RingbufferContainer is responsible for the functionalities supporting
 * the underlying ringbuffer structure containing the data.
 * This includes creating the actual ringbuffer, handling optional ringbuffer
 * storage, keeping the wait/notify key for blocking operations, ringbuffer
 * item expiration and other things not related to the ringbuffer data
 * structure.
 *
 * @param  the type of items in the ringbuffer container
 * @param  the type of items in the ringbuffer
 */
@SuppressWarnings("checkstyle:methodcount")
public class RingbufferContainer implements IdentifiedDataSerializable, Notifier {

    private static final long TTL_DISABLED = 0;

    private ObjectNamespace namespace;

    // a cached version of the wait notify key needed to wait for a change if the ringbuffer is empty
    private RingbufferWaitNotifyKey emptyRingWaitNotifyKey;
    /**
     * The expirationPolicy contains the expiration policy of the items. If a
     * time to live is set, the policy is created, otherwise it is {@code null}
     * to save space.
     */
    private RingbufferExpirationPolicy expirationPolicy;
    private InMemoryFormat inMemoryFormat;
    private RingbufferConfig config;
    private RingbufferStoreWrapper store;
    private SerializationService serializationService;

    /**
     * The ringbuffer containing the items. The type of contained items depends
     * on the {@link #inMemoryFormat} :
     * 
    *
  • {@link InMemoryFormat#OBJECT} - the type is the same as the type * {@link T}
  • *
  • {@link InMemoryFormat#BINARY} or {@link InMemoryFormat#NATIVE} - * the type is {@link Data}
  • *
*/ private Ringbuffer ringbuffer; /** * For purposes of {@link IdentifiedDataSerializable} instance creation. * For any other purpose, use other constructors in this class. */ public RingbufferContainer() { } /** * Constructs the ring buffer container with only the name and the key for * blocking operations. This does not fully prepare the container for usage. * The caller must invoke the * {@link #init(RingbufferConfig, NodeEngine)} * method to complete the initialization before usage. * * @param namespace the namespace of the ringbuffer container */ public RingbufferContainer(ObjectNamespace namespace, int partitionId) { this.namespace = namespace; this.emptyRingWaitNotifyKey = new RingbufferWaitNotifyKey(namespace, partitionId); } /** * Constructs a fully initialized ringbuffer that can be used immediately. * References to other services, the ringbuffer store, expiration policy * and the config are all set. * * @param namespace the namespace of the ring buffer container * @param config the configuration of the ring buffer * @param nodeEngine the NodeEngine */ public RingbufferContainer(ObjectNamespace namespace, RingbufferConfig config, NodeEngine nodeEngine, int partitionId) { this(namespace, partitionId); this.inMemoryFormat = config.getInMemoryFormat(); this.ringbuffer = new ArrayRingbuffer(config.getCapacity()); final long ttlMs = SECONDS.toMillis(config.getTimeToLiveSeconds()); if (ttlMs != TTL_DISABLED) { this.expirationPolicy = new RingbufferExpirationPolicy(ringbuffer.getCapacity(), ttlMs); } init(config, nodeEngine); } /** * Initializes the ring buffer with references to other services, the * ringbuffer store and the config. This is because on a replication * operation the container is only partially constructed. The init method * finishes the configuration of the ring buffer container for further * usage. * * @param config the configuration of the ring buffer * @param nodeEngine the NodeEngine */ public void init(RingbufferConfig config, NodeEngine nodeEngine) { this.config = config; this.serializationService = nodeEngine.getSerializationService(); initRingbufferStore(nodeEngine.getConfigClassLoader()); } private void initRingbufferStore(ClassLoader configClassLoader) { this.store = RingbufferStoreWrapper.create(namespace, config.getRingbufferStoreConfig(), config.getInMemoryFormat(), serializationService, configClassLoader); if (store.isEnabled()) { try { final long storeSequence = store.getLargestSequence(); if (tailSequence() < storeSequence) { ringbuffer.setTailSequence(storeSequence); ringbuffer.setHeadSequence(storeSequence + 1); } } catch (Exception e) { throw new HazelcastException(e); } } } public RingbufferStoreWrapper getStore() { return store; } /** * Gets the wait/notify key for the blocking operations of reading from the ring buffer. * * @return the wait/notify key */ public RingbufferWaitNotifyKey getRingEmptyWaitNotifyKey() { return emptyRingWaitNotifyKey; } public RingbufferConfig getConfig() { return config; } public long tailSequence() { return ringbuffer.tailSequence(); } public long headSequence() { return ringbuffer.headSequence(); } /** * Sets the head sequence. The head sequence cannot be larger than * {@code tailSequence() + 1} * * @param sequence the new head sequence * @throws IllegalArgumentException if the target sequence is greater than {@code tailSequence() + 1} * @see #tailSequence() */ public void setHeadSequence(long sequence) { ringbuffer.setHeadSequence(sequence); } /** * Sets the tail sequence. The tail sequence cannot be less than * {@code headSequence() - 1}. * * @param sequence the new tail sequence * @throws IllegalArgumentException if the target sequence is less than * {@code headSequence() - 1} * @see #headSequence() */ public void setTailSequence(long sequence) { ringbuffer.setTailSequence(sequence); } /** * Returns the capacity of this ringbuffer. * * @return the capacity */ public long getCapacity() { return ringbuffer.getCapacity(); } public long size() { return ringbuffer.size(); } // not used in the codebase, here just for API completion public boolean isEmpty() { return ringbuffer.isEmpty(); } /** * Returns if the sequence is one after the sequence of the newest item in * the ring buffer. In this case, the caller needs to wait for the item to * be available. This method will throw an exception if the requested * instance is larger than the sequence of the next item to be added (e.g. * the second or third next item to added). * If the ring buffer store is configured and enabled, this method will * allow sequence values older than the head sequence. * * @param sequence the requested sequence * @return should the caller wait for * @throws StaleSequenceException if the requested sequence is * 1. greater than the tail sequence + 1 or * 2. smaller than the head sequence and the data store is not enabled */ public boolean shouldWait(long sequence) { checkBlockableReadSequence(sequence); return sequence == ringbuffer.tailSequence() + 1; } /** * Returns the remaining capacity of the ringbuffer. If TTL is enabled, * then the returned capacity is equal to the total capacity of the * ringbuffer minus the number of used slots in the ringbuffer which have * not yet been marked as expired and cleaned up. Keep in mind that some * slots could have expired items that have not yet been cleaned up and * that the returned value could be stale as soon as it is returned. *

* If TTL is disabled, the remaining capacity is equal to the total * ringbuffer capacity. * * @return the remaining capacity of the ring buffer */ public long remainingCapacity() { if (expirationPolicy != null) { return ringbuffer.getCapacity() - size(); } return ringbuffer.getCapacity(); } /** * Adds one item to the ring buffer. Sets the expiration time if TTL is * configured and also attempts to store the item in the data store if one * is configured. The provided {@code item} can be {@link Data} or the * deserialized object. * The provided item will be transformed to the configured ringbuffer * {@link InMemoryFormat} if necessary. * * @param item item to be stored in the ring buffer and data store, can be * {@link Data} or an deserialized object * @return the sequence ID of the item stored in the ring buffer * @throws HazelcastException if there was any exception thrown by the data store * @throws HazelcastSerializationException if the ring buffer is configured to keep items in object format and the * item could not be deserialized */ public long add(T item) { final long nextSequence = ringbuffer.peekNextTailSequence(); if (store.isEnabled()) { try { store.store(nextSequence, convertToData(item)); } catch (Exception e) { throw new HazelcastException(e); } } final long storedSequence = addInternal(item); if (storedSequence != nextSequence) { throw new IllegalStateException("Sequence we stored the item with and Ringbuffer sequence differs. Was the " + "Ringbuffer mutated from multiple threads?"); } return storedSequence; } /** * Adds all items to the ringbuffer. Sets the expiration time if TTL is * configured and also attempts to store the items in the data store if one * is configured. * * @param items items to be stored in the ring buffer and data store * @return the sequence ID of the last item stored in the ring buffer * @throws HazelcastException if there was any exception thrown by the data store * @throws HazelcastSerializationException if the ring buffer is configured to keep items * in object format and the item could not be * deserialized */ public long addAll(T[] items) { long firstSequence = ringbuffer.peekNextTailSequence(); long lastSequence = ringbuffer.peekNextTailSequence(); if (store.isEnabled() && items.length != 0) { try { store.storeAll(firstSequence, convertToData(items)); } catch (Exception e) { throw new HazelcastException(e); } } for (int i = 0; i < items.length; i++) { lastSequence = addInternal(items[i]); } return lastSequence; } /** * Sets the item at the given sequence ID and updates the expiration time * if TTL is configured. * Unlike other methods for adding items into the ring buffer, does not * attempt to store the item in the data store. This method expands the * ring buffer tail and head sequence to accommodate for the sequence. * This means that it will move the head or tail sequence to the target * sequence if the target sequence is less than the head sequence or * greater than the tail sequence. * * @param sequenceId the sequence ID under which the item is stored * @param item item to be stored in the ring buffer and data store * @throws HazelcastSerializationException if the ring buffer is configured to keep items in object format and the * item could not be deserialized */ @SuppressWarnings("unchecked") public void set(long sequenceId, T item) { final E rbItem = convertToRingbufferFormat(item); // first we write the dataItem in the ring. ringbuffer.set(sequenceId, rbItem); if (sequenceId > tailSequence()) { ringbuffer.setTailSequence(sequenceId); if (ringbuffer.size() > ringbuffer.getCapacity()) { ringbuffer.setHeadSequence(ringbuffer.tailSequence() - ringbuffer.getCapacity() + 1); } } if (sequenceId < headSequence()) { ringbuffer.setHeadSequence(sequenceId); } // and then we optionally write the expiration. if (expirationPolicy != null) { expirationPolicy.setExpirationAt(sequenceId); } } /** * Reads one item from the ring buffer and returns the serialized format. * If the item is not available, it will try and load it from the ringbuffer store. * If the stored format is already serialized, there is no serialization. * * @param sequence The sequence of the item to be read * @return The item read * @throws StaleSequenceException if the sequence is : * 1. larger than the tailSequence or * 2. smaller than the headSequence and the data store is disabled */ public Data readAsData(long sequence) { checkReadSequence(sequence); Object rbItem = readOrLoadItem(sequence); return serializationService.toData(rbItem); } /** * Reads multiple items from the ring buffer and adds them to {@code result} * in the stored format. If an item is not available, it will try and * load it from the ringbuffer store. * * @param beginSequence the sequence of the first item to read. * @param result the List where the result are stored in. * @return returns the sequenceId of the next item to read. This is needed if not all required items are found. * @throws StaleSequenceException if the sequence is : * 1. larger than the tailSequence or * 2. smaller than the headSequence and the data store is disabled */ public long readMany(long beginSequence, ReadResultSetImpl result) { checkReadSequence(beginSequence); long seq = beginSequence; while (seq <= ringbuffer.tailSequence()) { result.addItem(seq, readOrLoadItem(seq)); seq++; if (result.isMaxSizeReached()) { // we have found all items we are looking for. We are done. break; } } return seq; } @SuppressWarnings("unchecked") public void cleanup() { if (expirationPolicy != null) { expirationPolicy.cleanup(ringbuffer); } } public boolean isStaleSequence(long sequence) { return sequence < headSequence() && !store.isEnabled(); } public boolean isTooLargeSequence(long sequence) { return sequence > tailSequence() + 1; } /** * Checks if the provided {@code sequence} can be read immediately. If * not, it will return the current oldest or newest immediately readable * sequence. This method can be used for a loss-tolerant reader when * trying to avoid a {@link com.hazelcast.ringbuffer.StaleSequenceException}. * * @param readSequence the sequence wanting to be read * @return the bounded sequence */ public long clampReadSequenceToBounds(long readSequence) { // fast forward if late and no store is configured final long headSequence = headSequence(); if (readSequence < headSequence && !store.isEnabled()) { return headSequence; } // jump back if too far in future final long tailSequence = tailSequence(); if (readSequence > tailSequence + 1) { return tailSequence + 1; } return readSequence; } /** * Check if the sequence is of an item that can be read immediately * or is the sequence of the next item to be added into the ringbuffer. * Since this method allows the sequence to be one larger than * the {@link #tailSequence()}, the caller can use this method * to check the sequence before performing a possibly blocking read. * Also, the requested sequence can be smaller than the head sequence * if the data store is enabled. * * @param readSequence the sequence wanting to be read * @throws StaleSequenceException if the requested sequence is smaller than the head sequence and the data store is * not enabled * @throws IllegalArgumentException if the requested sequence is greater than the tail sequence + 1 or */ public void checkBlockableReadSequence(long readSequence) { if (isTooLargeSequence(readSequence)) { throw new IllegalArgumentException("sequence:" + readSequence + " is too large. The current tailSequence is:" + tailSequence()); } if (isStaleSequence(readSequence)) { throw new StaleSequenceException("sequence:" + readSequence + " is too small and data store is disabled. " + "The current headSequence is:" + headSequence() + " tailSequence is:" + tailSequence(), headSequence()); } } /** * Check if the sequence can be read from the ring buffer. * * @param sequence the sequence wanting to be read * @throws StaleSequenceException if the requested sequence is smaller than the head sequence and the data * store is not enabled * @throws IllegalArgumentException if the requested sequence is greater than the tail sequence */ private void checkReadSequence(long sequence) { final long tailSequence = ringbuffer.tailSequence(); if (sequence > tailSequence) { throw new IllegalArgumentException("sequence:" + sequence + " is too large. The current tailSequence is:" + tailSequence); } if (isStaleSequence(sequence)) { throw new StaleSequenceException("sequence:" + sequence + " is too small and data store is disabled." + " The current headSequence is:" + headSequence() + " tailSequence is:" + tailSequence, headSequence()); } } /** * Reads the item at the specified sequence or loads it from the ringbuffer * store if one is enabled. The type of the returned object is equal to the * ringbuffer format. */ private Object readOrLoadItem(long sequence) { Object item; if (sequence < ringbuffer.headSequence() && store.isEnabled()) { item = store.load(sequence); } else { item = ringbuffer.read(sequence); } return item; } @SuppressWarnings("unchecked") private long addInternal(T item) { final E rbItem = convertToRingbufferFormat(item); // first we write the dataItem in the ring. final long tailSequence = ringbuffer.add(rbItem); // and then we optionally write the expiration. if (expirationPolicy != null) { expirationPolicy.setExpirationAt(tailSequence); } return tailSequence; } /** * Converts the {@code item} into the ringbuffer {@link InMemoryFormat} or * keeps it unchanged if the supplied argument is already in the ringbuffer * format. * * @param item the item * @return the binary or deserialized format, depending on the {@link RingbufferContainer#inMemoryFormat} * @throws HazelcastSerializationException if the ring buffer is configured to keep items * in object format and the item could not be deserialized */ private E convertToRingbufferFormat(Object item) { return inMemoryFormat == OBJECT ? (E) serializationService.toObject(item) : (E) serializationService.toData(item); } /** * Convert the supplied argument into serialized format. * * @throws HazelcastSerializationException when serialization fails. */ private Data convertToData(Object item) { return serializationService.toData(item); } /** * Convert the supplied argument into serialized format. * * @throws HazelcastSerializationException when serialization fails. */ private Data[] convertToData(T[] items) { if (items == null || items.length == 0) { return new Data[0]; } if (items[0] instanceof Data) { return (Data[]) items; } final Data[] ret = new Data[items.length]; for (int i = 0; i < items.length; i++) { ret[i] = convertToData(items[i]); } return ret; } @Override public void writeData(ObjectDataOutput out) throws IOException { boolean ttlEnabled = expirationPolicy != null; out.writeLong(ringbuffer.tailSequence()); out.writeLong(ringbuffer.headSequence()); out.writeInt((int) ringbuffer.getCapacity()); out.writeLong(ttlEnabled ? expirationPolicy.getTtlMs() : 0); out.writeInt(inMemoryFormat.ordinal()); long now = System.currentTimeMillis(); // we only write the actual content of the ringbuffer. So we don't write empty slots. for (long seq = ringbuffer.headSequence(); seq <= ringbuffer.tailSequence(); seq++) { if (inMemoryFormat == BINARY) { IOUtil.writeData(out, (Data) ringbuffer.read(seq)); } else { out.writeObject(ringbuffer.read(seq)); } // we write the time difference compared to now. Because the clock on the receiving side // can be totally different then our time. If there would be a ttl of 10 seconds, than // the expiration time would be 10 seconds after the insertion time. But the if ringbuffer is // migrated to a machine with a time 1 hour earlier, the ttl would effectively become // 1 hours and 10 seconds. if (ttlEnabled) { long deltaMs = expirationPolicy.getExpirationAt(seq) - now; out.writeLong(deltaMs); } } } @Override @SuppressWarnings("unchecked") public void readData(ObjectDataInput in) throws IOException { final long tailSequence = in.readLong(); final long headSequence = in.readLong(); final int capacity = in.readInt(); final long ttlMs = in.readLong(); inMemoryFormat = values()[in.readInt()]; ringbuffer = new ArrayRingbuffer(capacity); ringbuffer.setTailSequence(tailSequence); ringbuffer.setHeadSequence(headSequence); boolean ttlEnabled = ttlMs != TTL_DISABLED; if (ttlEnabled) { this.expirationPolicy = new RingbufferExpirationPolicy(capacity, ttlMs); } long now = System.currentTimeMillis(); for (long seq = headSequence; seq <= tailSequence; seq++) { if (inMemoryFormat == BINARY) { ringbuffer.set(seq, (E) IOUtil.readData(in)); } else { ringbuffer.set(seq, (E) in.readObject()); } if (ttlEnabled) { long delta = in.readLong(); expirationPolicy.setExpirationAt(seq, delta + now); } } } /** * Returns the ringbuffer containing the actual items. */ public Ringbuffer getRingbuffer() { return ringbuffer; } RingbufferExpirationPolicy getExpirationPolicy() { return expirationPolicy; } public ObjectNamespace getNamespace() { return namespace; } @Override public int getFactoryId() { return RingbufferDataSerializerHook.F_ID; } @Override public int getClassId() { return RingbufferDataSerializerHook.RINGBUFFER_CONTAINER; } @Override public boolean shouldNotify() { return true; } @Override public WaitNotifyKey getNotifiedKey() { return emptyRingWaitNotifyKey; } /** * Clears the data in the ringbuffer. */ public void clear() { ringbuffer.clear(); if (expirationPolicy != null) { expirationPolicy.clear(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy