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

zipkin2.server.internal.throttle.ThrottledStorageComponent Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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
 *
 *     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 zipkin2.server.internal.throttle;

import com.netflix.concurrency.limits.Limit;
import com.netflix.concurrency.limits.Limiter;
import com.netflix.concurrency.limits.limit.Gradient2Limit;
import com.netflix.concurrency.limits.limiter.AbstractLimiter;
import io.micrometer.core.instrument.MeterRegistry;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import zipkin2.Call;
import zipkin2.Span;
import zipkin2.storage.SpanConsumer;
import zipkin2.storage.SpanStore;
import zipkin2.storage.StorageComponent;

/**
 * Delegating implementation that limits requests to the {@link #spanConsumer()} of another {@link
 * StorageComponent}.  The theory here is that this class can be used to:
 * 
    *
  • Prevent spamming the storage engine with excessive, spike requests when they come in; thus * preserving it's life.
  • *
  • Optionally act as a buffer so that a fixed number requests can be queued for execution when * the throttle allows for it. This optional queue must be bounded in order to avoid running out of * memory from infinitely queueing.
  • *
* * @see ThrottledSpanConsumer */ public final class ThrottledStorageComponent extends StorageComponent { final StorageComponent delegate; final AbstractLimiter limiter; final ThreadPoolExecutor executor; public ThrottledStorageComponent(StorageComponent delegate, MeterRegistry registry, int minConcurrency, int maxConcurrency, int maxQueueSize) { this.delegate = Objects.requireNonNull(delegate); Limit limit = Gradient2Limit.newBuilder() .minLimit(minConcurrency) .initialLimit( minConcurrency) // Limiter will trend towards min until otherwise necessary so may as well start there .maxConcurrency(maxConcurrency) .queueSize(0) .build(); this.limiter = new Builder().limit(limit).build(); // TODO: explain these parameters this.executor = new ThreadPoolExecutor(limit.getLimit(), limit.getLimit(), 0, TimeUnit.DAYS, createQueue(maxQueueSize), new ThottledThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); limit.notifyOnChange(new ThreadPoolExecutorResizer(executor)); ActuateThrottleMetrics metrics = new ActuateThrottleMetrics(registry); metrics.bind(executor); metrics.bind(limiter); } @Override public SpanStore spanStore() { return delegate.spanStore(); } @Override public SpanConsumer spanConsumer() { return new ThrottledSpanConsumer(delegate.spanConsumer(), limiter, executor); } @Override public void close() throws IOException { executor.shutdownNow(); delegate.close(); } @Override public String toString() { return "Throttled(" + delegate + ")"; } static final class ThrottledSpanConsumer implements SpanConsumer { final SpanConsumer delegate; final Limiter limiter; final ExecutorService executor; ThrottledSpanConsumer(SpanConsumer delegate, Limiter limiter, ExecutorService executor) { this.delegate = delegate; this.limiter = limiter; this.executor = executor; } @Override public Call accept(List spans) { return new ThrottledCall<>(executor, limiter, delegate.accept(spans)); } @Override public String toString() { return "Throttled(" + delegate + ")"; } } static BlockingQueue createQueue(int maxSize) { if (maxSize < 0) throw new IllegalArgumentException("maxSize < 0"); if (maxSize == 0) { // 0 means we should be bounded but we can't create a queue with that size so use 1 instead. maxSize = 1; } return new LinkedBlockingQueue<>(maxSize); } static final class ThottledThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setDaemon(true); thread.setName("zipkin-throttle-pool-" + thread.getId()); return thread; } } static final class ThreadPoolExecutorResizer implements Consumer { final ThreadPoolExecutor executor; ThreadPoolExecutorResizer(ThreadPoolExecutor executor) { this.executor = executor; } /** * This is {@code synchronized} to ensure that we don't let the core/max pool sizes get out of * sync; even for an instant. The two need to be tightly coupled together to ensure that when * our queue fills up we don't spin up extra Threads beyond our calculated limit. * *

There is also an unfortunate aspect where the {@code max} has to always be greater than * {@code core} or an exception will be thrown. So they have to be adjust appropriately * relative to the direction the size is going. */ @Override public synchronized void accept(Integer newValue) { int previousValue = executor.getCorePoolSize(); int newValueInt = newValue; if (previousValue < newValueInt) { executor.setMaximumPoolSize(newValueInt); executor.setCorePoolSize(newValueInt); } else if (previousValue > newValueInt) { executor.setCorePoolSize(newValueInt); executor.setMaximumPoolSize(newValueInt); } // Note: no case for equals. Why modify something that doesn't need modified? } } static final class Builder extends AbstractLimiter.Builder { NonLimitingLimiter build() { return new NonLimitingLimiter(this); } @Override protected Builder self() { return this; } } /** * Unlike a normal Limiter, this will actually not prevent the creation of a {@link Listener} in * {@link #acquire(java.lang.Void)}. The point of this is to ensure that we can always derive an * appropriate {@link Limit#getLimit() Limit} while the {@link #executor} handles actually * limiting running requests. */ static final class NonLimitingLimiter extends AbstractLimiter { NonLimitingLimiter(AbstractLimiter.Builder builder) { super(builder); } @Override public Optional acquire(Void context) { return Optional.of(createListener()); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy