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

zipkin2.internal.DelayLimiter 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.internal;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/** Limits invocations of a given context to at most once per period. */
// this is a dependency-free variant formerly served by an expiring guava cache
public final class DelayLimiter {

  public static  DelayLimiter create() {
    return new Builder().build();
  }

  public static Builder newBuilder() {
    return new Builder();
  }

  public static final class Builder {
    Ticker ticker = new Ticker();
    long ttlNanos = TimeUnit.HOURS.toNanos(1); // legacy default from cassandra
    int cardinality = 5 * 4000; // Ex. 5 site tags with cardinality 4000 each

    /**
     * When {@link #shouldInvoke(Object)} returns true, it will return false until this duration
     * expires.
     */
    public Builder ttl(int ttl) {
      if (ttl <= 0) throw new IllegalArgumentException("ttl <= 0");
      this.ttlNanos = TimeUnit.MILLISECONDS.toNanos(ttl);
      return this;
    }

    /**
     * This bounds suppressions, useful because contexts can be accidentally unlimited cardinality.
     */
    public Builder cardinality(int cardinality) {
      if (cardinality <= 0) throw new IllegalArgumentException("cardinality <= 0");
      this.cardinality = cardinality;
      return this;
    }

    Builder ticker(Ticker ticker) { // do not expose public: only for tests
      this.ticker = ticker;
      return this;
    }

    public  DelayLimiter build() {
      return new DelayLimiter<>(this);
    }

    Builder() {
    }
  }

  static class Ticker { // not final for tests
    long read() {
      return System.nanoTime();
    }
  }

  final Ticker ticker;
  final ConcurrentHashMap> cache = new ConcurrentHashMap<>();
  final DelayQueue> suppressions = new DelayQueue<>();
  final long ttlNanos, cardinality;

  DelayLimiter(Builder builder) {
    ticker = builder.ticker;
    ttlNanos = builder.ttlNanos;
    cardinality = builder.cardinality;
  }

  /** Returns true if a given context should be invoked. */
  public boolean shouldInvoke(C context) {
    cleanupExpiredSuppressions();

    if (cache.containsKey(context)) return false;

    Suppression suppression = new Suppression<>(ticker, context, ticker.read() + ttlNanos);

    if (cache.putIfAbsent(context, suppression) != null) return false; // lost race

    suppressions.offer(suppression);

    // If we added an entry, it could make us go over the max size.
    if (suppressions.size() > cardinality) removeOneSuppression();

    return true;
  }

  void removeOneSuppression() {
    Suppression eldest;
    while ((eldest = suppressions.peek()) != null) { // loop unless empty
      if (suppressions.remove(eldest)) { // check for lost race
        cache.remove(eldest.context, eldest);
        break; // to ensure we don't remove two!
      }
    }
  }

  public void invalidate(C context) {
    Suppression suppression = cache.remove(context);
    if (suppression != null) suppressions.remove(suppression);
  }

  public void clear() {
    cache.clear();
    suppressions.clear();
  }

  void cleanupExpiredSuppressions() {
    Suppression expiredSuppression;
    while ((expiredSuppression = suppressions.poll()) != null) {
      cache.remove(expiredSuppression.context, expiredSuppression);
    }
  }

  static final class Suppression implements Delayed {
    final Ticker ticker;
    final C context;
    final long expiration;

    Suppression(Ticker ticker, C context, long expiration) {
      this.ticker = ticker;
      this.context = context;
      this.expiration = expiration;
    }

    @Override public long getDelay(TimeUnit unit) {
      return unit.convert(expiration - ticker.read(), TimeUnit.NANOSECONDS);
    }

    @Override public int compareTo(Delayed o) {
      return Long.signum(expiration - ((Suppression) o).expiration);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy