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

com.bazaarvoice.emodb.event.db.astyanax.DefaultSlabAllocator Maven / Gradle / Ivy

There is a newer version: 6.5.190
Show newest version
package com.bazaarvoice.emodb.event.db.astyanax;

import com.bazaarvoice.emodb.common.dropwizard.lifecycle.LifeCycleRegistry;
import com.bazaarvoice.emodb.common.uuid.TimeUUIDs;
import com.bazaarvoice.emodb.event.core.MetricsGroupName;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import com.google.common.collect.PeekingIterator;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.Inject;
import com.netflix.astyanax.serializers.TimeUUIDSerializer;
import io.dropwizard.lifecycle.ExecutorServiceManager;
import io.dropwizard.util.Duration;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

import java.nio.ByteBuffer;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

public class DefaultSlabAllocator implements SlabAllocator {
    private final ManifestPersister _persister;
    private final LoadingCache _channelStateCache;
    private final Meter _inactiveSlabMeter;

    @Inject
    public DefaultSlabAllocator(LifeCycleRegistry lifeCycle, ManifestPersister persister, @MetricsGroupName String metricsGroup, MetricRegistry metricRegistry) {
        _persister = persister;
        _channelStateCache = CacheBuilder.newBuilder()
                // Close each slab after a period of inactivity well before the reader slab timeout.  We need to be
                // careful about causing race conditions when closing slabs--if a slab is closed while there's any
                // chance writers are still writing, we could lose events.  So we only close slabs once (a) they're
                // full or (b) they haven't been accessed in a while.  We don't close slabs at shutdown because it's
                // hard to be completely sure all writers have quiesced.
                .expireAfterAccess(Constants.OPEN_SLAB_MARKER_TTL.toMillis() / 2, TimeUnit.MILLISECONDS)
                .removalListener(new RemovalListener() {
                    @Override
                    public void onRemoval(RemovalNotification notification) {
                        closeChannelState(notification.getValue());
                    }
                })
                .build(new CacheLoader() {
                    @Override
                    public ChannelAllocationState load(String channel) throws Exception {
                        return new ChannelAllocationState();
                    }
                });
        // Cleanup the cache once a minute to ensure channels are closed soon after they become inactive so when
        // readers detect "slate" open slabs it's because a writer has crashed, not that the writer is relatively idle.
        defaultExecutor(lifeCycle, metricsGroup).scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                _channelStateCache.cleanUp();
            }
        }, 1, 1, TimeUnit.MINUTES);
        _inactiveSlabMeter = metricRegistry.meter(MetricRegistry.name(metricsGroup, "DefaultSlabAllocator", "inactive_slabs"));
    }

    private static ScheduledExecutorService defaultExecutor(LifeCycleRegistry lifeCycle, String metricsGroup) {
        String nameFormat = "Events Slab Allocator Cleanup-" + metricsGroup.substring(metricsGroup.lastIndexOf('.') + 1) + "-%d";
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(nameFormat).setDaemon(true).build();
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1, threadFactory);
        lifeCycle.manage(new ExecutorServiceManager(executor, Duration.seconds(5), nameFormat));
        return executor;
    }

    /** Compute how many slots in a slab will be used to do a default (new slab) allocation, and how many bytes it will consume
     *
     * @param slabSlotsUsed - number of available slots in a slab that have been used prior to this allocation
     * @param slabBytesUsed - number of bytes in a slab that have been used prior to this allocation
     * @param eventSizes - list of the size in bytes of all events that we want to insert into the slab
     *
     * @return a pair of integers, the left value is the number of slots that will be used by this allocation and the
     *         right value is the numb er of bytes that will be used by this allocation
     */
    static Pair defaultAllocationCount(int slabSlotsUsed, int slabBytesUsed, PeekingIterator eventSizes) {
        int slabTotalSlotCount = slabSlotsUsed;
        int allocationSlotCount = 0;
        int slabTotalBytesUsed = slabBytesUsed;
        int allocationBytes = 0;
        while (eventSizes.hasNext()) {
            checkArgument(eventSizes.peek() <= Constants.MAX_EVENT_SIZE_IN_BYTES, "Event size (" + eventSizes.peek() + ") is greater than the maximum allowed (" + Constants.MAX_EVENT_SIZE_IN_BYTES + ") event size");
            if (slabTotalSlotCount + 1 <= Constants.MAX_SLAB_SIZE && slabTotalBytesUsed + eventSizes.peek() <= Constants.MAX_SLAB_SIZE_IN_BYTES) {
                slabTotalSlotCount++;
                allocationSlotCount++;
                int eventSize = eventSizes.next();
                slabTotalBytesUsed += eventSize;
                allocationBytes += eventSize;
            } else {
                break;
            }
        }
        return new ImmutablePair<>(allocationSlotCount, allocationBytes);
    }

    @Override
    public SlabAllocation allocate(String channelName, int desiredCount, PeekingIterator eventSizes) {
        requireNonNull(channelName, "channelName");
        checkArgument(desiredCount > 0, "desiredCount must be >0");

        ChannelAllocationState channelState = _channelStateCache.getUnchecked(channelName);

        // Rotate slabs on a periodic basis so slab entries don't expire after the manifest
        channelState.rotateIfNecessary();

        // Scenarios:
        // - Slab is open and has space.  (Any open slab must have space.)
        //   - Synchronously allocate from remaining space and return.
        // - Slab is closed and requester wants >= MAX_SLAB_SIZE events
        //   - Generate a private slab ID, persist it, return the entire slab.
        // - Slab is closed and requester wants < MAX_SLAB_SIZE events
        //   - Synchronously generate slab ID, persist it, allocate from remaining space and return.

        if (desiredCount >= Constants.MAX_SLAB_SIZE) {
            // Special case for callers writing lots and lots of events.  They don't have to synchronize on the
            // SlabPersister I/O operation around creating slabs.

            // Is there is an existing open slab we can allocate from?
            SlabAllocation allocation = channelState.allocate(eventSizes);
            if (allocation != null) {
                return allocation;
            }

            // No existing slab.  Create a new slab just for the caller and not shared with anyone else.
            SlabRef slab = createSlab(channelName);
            return new DefaultSlabAllocation(slab, 0, defaultAllocationCount(0, 0, eventSizes).getLeft());

        } else {
            // Regular case for callers writing a few events.  Allocate from an existing open slab if possible, and if
            // not synchronize on the SlabPersister I/O operation of creating a new slab so only one slab is created
            // per channel at a time.
            synchronized (channelState.getSlabCreationLock()) {
                // Can we satisfy the allocation without creating a new slab?
                SlabAllocation allocation = channelState.allocate(eventSizes);
                if (allocation != null) {
                    return allocation;
                }

                // Must create a new slab.
                SlabRef slab = createSlab(channelName);

                // We're still guaranteed that the channel is closed (!channel.isAttached()) because all calls to the
                // channel.attach() method are protected by the SlabCreationLock which we held when checking isAttached.
                return channelState.attachAndAllocate(slab, eventSizes);
            }
        }
    }

    private void closeChannelState(ChannelAllocationState channelState) {
        SlabRef slab = channelState.detach();
        if (slab != null) {
            slab.release();
            _inactiveSlabMeter.mark();
        }
    }

    private SlabRef createSlab(String channel) {
        return new SlabRef(channel, generateSlabId(), _persister);
    }

    private ByteBuffer generateSlabId() {
        return TimeUUIDSerializer.get().toByteBuffer(TimeUUIDs.newUUID());
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy