package io.kaif.flake; import java.time.Clock; import java.time.Instant; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import com.google.common.annotations.VisibleForTesting; /** * for every unique id domain usage, you should create it's own sub class, for example: * <p> * you have two domains that require unique id, and they are allowed to overlay (because different * domain), you can create two subclass: * <p> * CommentFlakeIdGenerator * ArticleFlakeIdGenerator * <p> * CommentFlakeIdGenerator is guarantee all flakeId generated for model (Comment) are unique. so * does ArticleFlakeIdGenerator. however, two types of generator may generate duplicate flakeId. * <p> * the same type of FlakeIdGenerator guarantee generated flakeIds are unique, no matter how many * instances of generator. */ public abstract class FlakeIdGenerator { private static final ConcurrentHashMap<String, AtomicLong> lastSequenceTimeByScope = new ConcurrentHashMap<>(); private static AtomicLong buildLastSequenceTime(String scope) { return lastSequenceTimeByScope.computeIfAbsent(scope, (theScope) -> new AtomicLong()); } private final int nodeId; private final Clock clock; private final AtomicLong lastSequenceTime; protected FlakeIdGenerator(int nodeId) { this.nodeId = nodeId; this.clock = null; this.lastSequenceTime = buildLastSequenceTime(getClass().toString()); } @VisibleForTesting FlakeIdGenerator(int nodeId, Clock clock, String scope) { this.nodeId = nodeId; this.clock = clock; this.lastSequenceTime = buildLastSequenceTime(scope); } private long currentEpochMilli() { if (clock == null) { return System.currentTimeMillis(); } return Instant.now(clock).toEpochMilli(); } public final FlakeId next() { return new FlakeId(getCurrentSequenceTime(), nodeId); } /* * ingram: The code copy from cassandra driver UUIDs class * * Note that currently we use System.currentTimeMillis() for a base time in * milliseconds, and then if we are in the same milliseconds that the * previous generation, we increment the number of sub milli second. * However, since the precision is 4096/milli-second (12 bit), we can only * generate 4096 FlakeId within a millisecond safely. If we detect we have * already generated that much FlakeId within a millisecond (which, while * admittedly unlikely in a real application, is very achievable on even * modest machines), then we stall the generator (busy spin) until the next * millisecond as required. */ private long getCurrentSequenceTime() { while (true) { long now = FlakeId.sequenceTimeFromEpochMilli(currentEpochMilli()); long last = lastSequenceTime.get(); if (now > last) { if (lastSequenceTime.compareAndSet(last, now)) { return now; } } else { long lastMillis = FlakeId.millisOfSequenceTime(last); // If the clock went back in time, bail out if (FlakeId.millisOfSequenceTime(now) < FlakeId.millisOfSequenceTime(last)) { return lastSequenceTime.incrementAndGet(); } long candidate = last + 1; // If we've generated more than 4096 FlakeId in that millisecond, // we restart the whole process until we get to the next millis. // Otherwise, we try use our candidate ... unless we've been // beaten by another thread in which case we try again. if (FlakeId.millisOfSequenceTime(candidate) == lastMillis && lastSequenceTime.compareAndSet( last, candidate)) { return candidate; } } } } public final int getNodeId() { return nodeId; } }