package act.util; /*- * #%L * ACT Framework * %% * Copyright (C) 2014 - 2017 ActFramework * %% * 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. * #L% */ import org.joda.time.DateTime; import org.osgl.$; import org.osgl.util.E; import org.osgl.util.IO; import org.osgl.util.S; import java.io.File; import java.util.concurrent.atomic.AtomicLong; /** * Generate unique ID in a cluster */ public class IdGenerator { /** * Implementation of {@code StartIdProvider} shall return a * unique id per each system start */ public interface StartIdProvider { /** * Returns the system start ID. The start ID shall be different * between System starts, but it shall remaining the same value * within one system life time */ long startId(); /** * Generate system start ID based on timestamp */ class Timestamp implements StartIdProvider { private final long id; public Timestamp() { long origin = DateTime.parse("2016-05-10").getMillis(); // let's assume the system cannot be restart within 10 seconds long l = ($.ms() - origin) / 1000 / 10; id = l; } @Override public long startId() { return id; } } /** * Generate system start ID based on incremental sequence. The newly generated ID * will be write to a File */ class FileBasedStartCounter implements StartIdProvider { private final long id; public FileBasedStartCounter() { this(".act.id-global"); } public FileBasedStartCounter(String path) { File file = new File(path); if (file.exists()) { String s = IO.readContentAsString(file); long seq = Long.parseLong(s); seq = seq + 1; IO.writeContent(S.str(seq), file); id = (seq); } else { id = 0; IO.writeContent(Long.toString(id), file); } } @Override public long startId() { return id; } } /** * Default start ID provider will try to use the {@link act.util.IdGenerator.StartIdProvider.FileBasedStartCounter}. In case * File IO is not allowed (e.g. in GAE), then it will use {@link act.util.IdGenerator.StartIdProvider.Timestamp} */ class DefaultStartIdProvider implements StartIdProvider { private StartIdProvider delegate; public DefaultStartIdProvider() { this(".act.id-global"); } public DefaultStartIdProvider(String path) { try { delegate = new FileBasedStartCounter(path); delegate.startId(); } catch (Exception e) { delegate = new Timestamp(); } } @Override public long startId() { return delegate.startId(); } } } /** * {@code SequenceProvider} shall generate unique ID within * one JVM per each call */ public interface SequenceProvider { long seqId(); class AtomicLongSeq implements SequenceProvider { private final AtomicLong seq = new AtomicLong(0); @Override public long seqId() { return (seq.incrementAndGet()); } } } public interface NodeIdProvider { long nodeId(); class IpProvider implements NodeIdProvider { private enum EffectiveBytes { ONE(1), TWO(2), THREE(3), FOUR(4); private int value; EffectiveBytes(int value) { this.value = value; } public static EffectiveBytes valueOf(int n) { switch (n) { case 1: return ONE; case 2: return TWO; case 3: return THREE; case 4: return FOUR; default : throw E.unexpected("Invalid EffectiveByte value: %s. Expected: 1 - 4", n); } } } private final EffectiveBytes effectiveBytes; private final long id; public IpProvider() { this(4); } public IpProvider(int effectBytes) { this.effectiveBytes = EffectiveBytes.valueOf(effectBytes); String ip = LocalIpAddressUtil.ip(); String[] sa = ip.split("\\."); int n = effectiveBytes.value; long l = 0; for (int i = 0; i < n; ++i) { String b = sa[3 - i]; long factor = 1; for (int j = 0; j < i; ++j) { factor = factor * 256; } l += Long.valueOf(b) * factor; } id = (l); } public long nodeId() { return id; } } } public interface LongEncoder { String longToStr(long l); abstract class LongEncoderBase implements LongEncoder { private final char[] digits; private final int MAX_RADIX; public LongEncoderBase(char[] digits) { this.digits = digits; this.MAX_RADIX = digits.length; } /** * Code copied from JDK Long.toString(long, String) */ public String longToStr(long l) { int radix = MAX_RADIX; char[] buf = new char[65]; int charPos = 64; boolean negative = (l < 0); if (!negative) { l = -l; } while (l <= -radix) { buf[charPos--] = digits[(int)(-(l % radix))]; l = l / radix; } buf[charPos] = digits[(int)(-l)]; if (negative) { buf[--charPos] = '-'; } return new String(buf, charPos, (65 - charPos)); } } } public static class UnsafeLongEncoder extends LongEncoder.LongEncoderBase { /** * Extended char table for representing a number as a String */ private final static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '!', '$', '%', '&', '.', ',', ';', ':', '=', '?', '+', '-', '*', '/', '<', '>', '_', '~', '#', '^', '@', '|', '(', ')', '[', ']', '{', '}' }; public UnsafeLongEncoder() { super(digits); } } public static class SafeLongEncoder extends LongEncoder.LongEncoderBase { /** * Extended char table for representing a number as a String */ private final static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '.', '-', '_', '~', }; public SafeLongEncoder() { super(digits); } } public static final LongEncoder SAFE_ENCODER = new SafeLongEncoder(); public static final LongEncoder UNSAFE_ENCODER = new UnsafeLongEncoder(); private final NodeIdProvider nodeIdProvider; private final StartIdProvider startIdProvider; private final SequenceProvider sequenceProvider; private LongEncoder longEncoder; /** * Create a default IdGenerator with following configuration: * <ul> * <li>Node ID provider: four byte IP address</li> * <li>Start ID provider: stored in <code>.act.id-global</code> file</li> * <li>Sequence ID provider: Atomic Long sequence</li> * <li>Long Encoder: {@link SafeLongEncoder}</li> * </ul> */ public IdGenerator() { this(new NodeIdProvider.IpProvider(), new StartIdProvider.DefaultStartIdProvider(), new SequenceProvider.AtomicLongSeq(), SAFE_ENCODER); } /** * Create a default IdGenerator with following configuration: * <ul> * <li>Node ID provider: four byte IP address</li> * <li>Start ID provider: stored in <code>.act.id-global</code> file</li> * <li>Sequence ID provider: Atomic Long sequence</li> * <li> * Long Encoder: {@link UnsafeLongEncoder} when `useUnsafeLongEncoder` is set to * `true` or {@link SafeLongEncoder} otherwise * </li> * </ul> * @param useUnsafeLongEncoder indicate use safe or unsafe long encoder */ public IdGenerator(boolean useUnsafeLongEncoder) { this(new NodeIdProvider.IpProvider(), new StartIdProvider.DefaultStartIdProvider(), new SequenceProvider.AtomicLongSeq(), useUnsafeLongEncoder ? UNSAFE_ENCODER : SAFE_ENCODER); } /** * Create a default IdGenerator with specified node id provider, start id provider and sequence provider: */ public IdGenerator(NodeIdProvider nodeIdProvider, StartIdProvider startIdProvider, SequenceProvider sequenceProvider, LongEncoder longEncoder) { this.nodeIdProvider = $.notNull(nodeIdProvider); this.startIdProvider = $.notNull(startIdProvider); this.sequenceProvider = $.notNull(sequenceProvider); this.longEncoder = $.notNull(longEncoder); } /** * Create a default IdGenerator with following configuration: * <ul> * <li>Node ID provider: N byte IP address, where N is specified by effectiveIpBytes argument</li> * <li>Start ID provider: stored in <code>.act.id-global</code> file</li> * <li>Sequnce ID provider: Atomic Long sequence</li> * </ul> */ public IdGenerator(int effectiveIpBytes) { this.nodeIdProvider = new NodeIdProvider.IpProvider(effectiveIpBytes); this.startIdProvider = new StartIdProvider.DefaultStartIdProvider(); this.sequenceProvider = new SequenceProvider.AtomicLongSeq(); this.longEncoder = SAFE_ENCODER; } /** * Create a default IdGenerator with following configuration: * <ul> * <li>Node ID provider: N byte IP address, where N is specified by effectiveIpBytes argument</li> * <li>Start ID provider: use start ID file specified by startIdFile argument</li> * <li>Sequnce ID provider: Atomic Long sequence</li> * </ul> */ public IdGenerator(int effectiveIpBytes, String startIdFile) { this.nodeIdProvider = new NodeIdProvider.IpProvider(effectiveIpBytes); this.startIdProvider = new StartIdProvider.DefaultStartIdProvider(startIdFile); this.sequenceProvider = new SequenceProvider.AtomicLongSeq(); this.longEncoder = SAFE_ENCODER; } /** * Create a default IdGenerator with following configuration: * <ul> * <li>Node ID provider: 4 byte IP address</li> * <li>Start ID provider: use start ID file specified by startIdFile argument</li> * <li>Sequnce ID provider: Atomic Long sequence</li> * </ul> */ public IdGenerator(String startIdFile) { this.nodeIdProvider = new NodeIdProvider.IpProvider(); this.startIdProvider = new StartIdProvider.DefaultStartIdProvider(startIdFile); this.sequenceProvider = new SequenceProvider.AtomicLongSeq(); this.longEncoder = SAFE_ENCODER; } /** * Generate a unique ID across the cluster * @return generated ID */ public String genId() { S.Buffer sb = S.newBuffer(); sb.a(longEncoder.longToStr(nodeIdProvider.nodeId())) .a(longEncoder.longToStr(startIdProvider.startId())) .a(longEncoder.longToStr(sequenceProvider.seqId())); return sb.toString(); } }