feat: 基于Snowflake算法生成有序数字UID

# 参考百度uid-generator进行改造符合我们自身的需求
This commit is contained in:
fit2-zhao 2023-09-04 17:16:51 +08:00 committed by fit2-zhao
parent f2da9e068d
commit ca58b0c2fa
25 changed files with 2302 additions and 6 deletions

View File

@ -0,0 +1,125 @@
package io.metersphere.sdk.domain;
import io.metersphere.validation.groups.Created;
import io.metersphere.validation.groups.Updated;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
@Data
public class WorkerNode implements Serializable {
@Schema(description = "auto increment id", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{worker_node.id.not_blank}", groups = {Updated.class})
private Long id;
@Schema(description = "host name", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{worker_node.host_name.not_blank}", groups = {Created.class})
@Size(min = 1, max = 64, message = "{worker_node.host_name.length_range}", groups = {Created.class, Updated.class})
private String hostName;
@Schema(description = "port", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{worker_node.port.not_blank}", groups = {Created.class})
@Size(min = 1, max = 64, message = "{worker_node.port.length_range}", groups = {Created.class, Updated.class})
private String port;
@Schema(description = "node type: ACTUAL or CONTAINER", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "{worker_node.type.not_blank}", groups = {Created.class})
private Integer type;
@Schema(description = "launch date", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "{worker_node.launch_date.not_blank}", groups = {Created.class})
private Long launchDate;
@Schema(description = "modified time", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "{worker_node.modified.not_blank}", groups = {Created.class})
private Long modified;
@Schema(description = "created time", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "{worker_node.created.not_blank}", groups = {Created.class})
private Long created;
private static final long serialVersionUID = 1L;
public enum Column {
id("id", "id", "BIGINT", false),
hostName("host_name", "hostName", "VARCHAR", false),
port("port", "port", "VARCHAR", false),
type("type", "type", "INTEGER", true),
launchDate("launch_date", "launchDate", "BIGINT", false),
modified("modified", "modified", "BIGINT", false),
created("created", "created", "BIGINT", false);
private static final String BEGINNING_DELIMITER = "`";
private static final String ENDING_DELIMITER = "`";
private final String column;
private final boolean isColumnNameDelimited;
private final String javaProperty;
private final String jdbcType;
public String value() {
return this.column;
}
public String getValue() {
return this.column;
}
public String getJavaProperty() {
return this.javaProperty;
}
public String getJdbcType() {
return this.jdbcType;
}
Column(String column, String javaProperty, String jdbcType, boolean isColumnNameDelimited) {
this.column = column;
this.javaProperty = javaProperty;
this.jdbcType = jdbcType;
this.isColumnNameDelimited = isColumnNameDelimited;
}
public String desc() {
return this.getEscapedColumnName() + " DESC";
}
public String asc() {
return this.getEscapedColumnName() + " ASC";
}
public static Column[] excludes(Column ... excludes) {
ArrayList<Column> columns = new ArrayList<>(Arrays.asList(Column.values()));
if (excludes != null && excludes.length > 0) {
columns.removeAll(new ArrayList<>(Arrays.asList(excludes)));
}
return columns.toArray(new Column[]{});
}
public static Column[] all() {
return Column.values();
}
public String getEscapedColumnName() {
if (this.isColumnNameDelimited) {
return new StringBuilder().append(BEGINNING_DELIMITER).append(this.column).append(ENDING_DELIMITER).toString();
} else {
return this.column;
}
}
public String getAliasedEscapedColumnName() {
return this.getEscapedColumnName();
}
}
}

View File

@ -0,0 +1,640 @@
package io.metersphere.sdk.domain;
import java.util.ArrayList;
import java.util.List;
public class WorkerNodeExample {
protected String orderByClause;
protected boolean distinct;
protected List<Criteria> oredCriteria;
public WorkerNodeExample() {
oredCriteria = new ArrayList<Criteria>();
}
public void setOrderByClause(String orderByClause) {
this.orderByClause = orderByClause;
}
public String getOrderByClause() {
return orderByClause;
}
public void setDistinct(boolean distinct) {
this.distinct = distinct;
}
public boolean isDistinct() {
return distinct;
}
public List<Criteria> getOredCriteria() {
return oredCriteria;
}
public void or(Criteria criteria) {
oredCriteria.add(criteria);
}
public Criteria or() {
Criteria criteria = createCriteriaInternal();
oredCriteria.add(criteria);
return criteria;
}
public Criteria createCriteria() {
Criteria criteria = createCriteriaInternal();
if (oredCriteria.size() == 0) {
oredCriteria.add(criteria);
}
return criteria;
}
protected Criteria createCriteriaInternal() {
Criteria criteria = new Criteria();
return criteria;
}
public void clear() {
oredCriteria.clear();
orderByClause = null;
distinct = false;
}
protected abstract static class GeneratedCriteria {
protected List<Criterion> criteria;
protected GeneratedCriteria() {
super();
criteria = new ArrayList<Criterion>();
}
public boolean isValid() {
return criteria.size() > 0;
}
public List<Criterion> getAllCriteria() {
return criteria;
}
public List<Criterion> getCriteria() {
return criteria;
}
protected void addCriterion(String condition) {
if (condition == null) {
throw new RuntimeException("Value for condition cannot be null");
}
criteria.add(new Criterion(condition));
}
protected void addCriterion(String condition, Object value, String property) {
if (value == null) {
throw new RuntimeException("Value for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value));
}
protected void addCriterion(String condition, Object value1, Object value2, String property) {
if (value1 == null || value2 == null) {
throw new RuntimeException("Between values for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value1, value2));
}
public Criteria andIdIsNull() {
addCriterion("id is null");
return (Criteria) this;
}
public Criteria andIdIsNotNull() {
addCriterion("id is not null");
return (Criteria) this;
}
public Criteria andIdEqualTo(Long value) {
addCriterion("id =", value, "id");
return (Criteria) this;
}
public Criteria andIdNotEqualTo(Long value) {
addCriterion("id <>", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThan(Long value) {
addCriterion("id >", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThanOrEqualTo(Long value) {
addCriterion("id >=", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThan(Long value) {
addCriterion("id <", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThanOrEqualTo(Long value) {
addCriterion("id <=", value, "id");
return (Criteria) this;
}
public Criteria andIdIn(List<Long> values) {
addCriterion("id in", values, "id");
return (Criteria) this;
}
public Criteria andIdNotIn(List<Long> values) {
addCriterion("id not in", values, "id");
return (Criteria) this;
}
public Criteria andIdBetween(Long value1, Long value2) {
addCriterion("id between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andIdNotBetween(Long value1, Long value2) {
addCriterion("id not between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andHostNameIsNull() {
addCriterion("host_name is null");
return (Criteria) this;
}
public Criteria andHostNameIsNotNull() {
addCriterion("host_name is not null");
return (Criteria) this;
}
public Criteria andHostNameEqualTo(String value) {
addCriterion("host_name =", value, "hostName");
return (Criteria) this;
}
public Criteria andHostNameNotEqualTo(String value) {
addCriterion("host_name <>", value, "hostName");
return (Criteria) this;
}
public Criteria andHostNameGreaterThan(String value) {
addCriterion("host_name >", value, "hostName");
return (Criteria) this;
}
public Criteria andHostNameGreaterThanOrEqualTo(String value) {
addCriterion("host_name >=", value, "hostName");
return (Criteria) this;
}
public Criteria andHostNameLessThan(String value) {
addCriterion("host_name <", value, "hostName");
return (Criteria) this;
}
public Criteria andHostNameLessThanOrEqualTo(String value) {
addCriterion("host_name <=", value, "hostName");
return (Criteria) this;
}
public Criteria andHostNameLike(String value) {
addCriterion("host_name like", value, "hostName");
return (Criteria) this;
}
public Criteria andHostNameNotLike(String value) {
addCriterion("host_name not like", value, "hostName");
return (Criteria) this;
}
public Criteria andHostNameIn(List<String> values) {
addCriterion("host_name in", values, "hostName");
return (Criteria) this;
}
public Criteria andHostNameNotIn(List<String> values) {
addCriterion("host_name not in", values, "hostName");
return (Criteria) this;
}
public Criteria andHostNameBetween(String value1, String value2) {
addCriterion("host_name between", value1, value2, "hostName");
return (Criteria) this;
}
public Criteria andHostNameNotBetween(String value1, String value2) {
addCriterion("host_name not between", value1, value2, "hostName");
return (Criteria) this;
}
public Criteria andPortIsNull() {
addCriterion("port is null");
return (Criteria) this;
}
public Criteria andPortIsNotNull() {
addCriterion("port is not null");
return (Criteria) this;
}
public Criteria andPortEqualTo(String value) {
addCriterion("port =", value, "port");
return (Criteria) this;
}
public Criteria andPortNotEqualTo(String value) {
addCriterion("port <>", value, "port");
return (Criteria) this;
}
public Criteria andPortGreaterThan(String value) {
addCriterion("port >", value, "port");
return (Criteria) this;
}
public Criteria andPortGreaterThanOrEqualTo(String value) {
addCriterion("port >=", value, "port");
return (Criteria) this;
}
public Criteria andPortLessThan(String value) {
addCriterion("port <", value, "port");
return (Criteria) this;
}
public Criteria andPortLessThanOrEqualTo(String value) {
addCriterion("port <=", value, "port");
return (Criteria) this;
}
public Criteria andPortLike(String value) {
addCriterion("port like", value, "port");
return (Criteria) this;
}
public Criteria andPortNotLike(String value) {
addCriterion("port not like", value, "port");
return (Criteria) this;
}
public Criteria andPortIn(List<String> values) {
addCriterion("port in", values, "port");
return (Criteria) this;
}
public Criteria andPortNotIn(List<String> values) {
addCriterion("port not in", values, "port");
return (Criteria) this;
}
public Criteria andPortBetween(String value1, String value2) {
addCriterion("port between", value1, value2, "port");
return (Criteria) this;
}
public Criteria andPortNotBetween(String value1, String value2) {
addCriterion("port not between", value1, value2, "port");
return (Criteria) this;
}
public Criteria andTypeIsNull() {
addCriterion("`type` is null");
return (Criteria) this;
}
public Criteria andTypeIsNotNull() {
addCriterion("`type` is not null");
return (Criteria) this;
}
public Criteria andTypeEqualTo(Integer value) {
addCriterion("`type` =", value, "type");
return (Criteria) this;
}
public Criteria andTypeNotEqualTo(Integer value) {
addCriterion("`type` <>", value, "type");
return (Criteria) this;
}
public Criteria andTypeGreaterThan(Integer value) {
addCriterion("`type` >", value, "type");
return (Criteria) this;
}
public Criteria andTypeGreaterThanOrEqualTo(Integer value) {
addCriterion("`type` >=", value, "type");
return (Criteria) this;
}
public Criteria andTypeLessThan(Integer value) {
addCriterion("`type` <", value, "type");
return (Criteria) this;
}
public Criteria andTypeLessThanOrEqualTo(Integer value) {
addCriterion("`type` <=", value, "type");
return (Criteria) this;
}
public Criteria andTypeIn(List<Integer> values) {
addCriterion("`type` in", values, "type");
return (Criteria) this;
}
public Criteria andTypeNotIn(List<Integer> values) {
addCriterion("`type` not in", values, "type");
return (Criteria) this;
}
public Criteria andTypeBetween(Integer value1, Integer value2) {
addCriterion("`type` between", value1, value2, "type");
return (Criteria) this;
}
public Criteria andTypeNotBetween(Integer value1, Integer value2) {
addCriterion("`type` not between", value1, value2, "type");
return (Criteria) this;
}
public Criteria andLaunchDateIsNull() {
addCriterion("launch_date is null");
return (Criteria) this;
}
public Criteria andLaunchDateIsNotNull() {
addCriterion("launch_date is not null");
return (Criteria) this;
}
public Criteria andLaunchDateEqualTo(Long value) {
addCriterion("launch_date =", value, "launchDate");
return (Criteria) this;
}
public Criteria andLaunchDateNotEqualTo(Long value) {
addCriterion("launch_date <>", value, "launchDate");
return (Criteria) this;
}
public Criteria andLaunchDateGreaterThan(Long value) {
addCriterion("launch_date >", value, "launchDate");
return (Criteria) this;
}
public Criteria andLaunchDateGreaterThanOrEqualTo(Long value) {
addCriterion("launch_date >=", value, "launchDate");
return (Criteria) this;
}
public Criteria andLaunchDateLessThan(Long value) {
addCriterion("launch_date <", value, "launchDate");
return (Criteria) this;
}
public Criteria andLaunchDateLessThanOrEqualTo(Long value) {
addCriterion("launch_date <=", value, "launchDate");
return (Criteria) this;
}
public Criteria andLaunchDateIn(List<Long> values) {
addCriterion("launch_date in", values, "launchDate");
return (Criteria) this;
}
public Criteria andLaunchDateNotIn(List<Long> values) {
addCriterion("launch_date not in", values, "launchDate");
return (Criteria) this;
}
public Criteria andLaunchDateBetween(Long value1, Long value2) {
addCriterion("launch_date between", value1, value2, "launchDate");
return (Criteria) this;
}
public Criteria andLaunchDateNotBetween(Long value1, Long value2) {
addCriterion("launch_date not between", value1, value2, "launchDate");
return (Criteria) this;
}
public Criteria andModifiedIsNull() {
addCriterion("modified is null");
return (Criteria) this;
}
public Criteria andModifiedIsNotNull() {
addCriterion("modified is not null");
return (Criteria) this;
}
public Criteria andModifiedEqualTo(Long value) {
addCriterion("modified =", value, "modified");
return (Criteria) this;
}
public Criteria andModifiedNotEqualTo(Long value) {
addCriterion("modified <>", value, "modified");
return (Criteria) this;
}
public Criteria andModifiedGreaterThan(Long value) {
addCriterion("modified >", value, "modified");
return (Criteria) this;
}
public Criteria andModifiedGreaterThanOrEqualTo(Long value) {
addCriterion("modified >=", value, "modified");
return (Criteria) this;
}
public Criteria andModifiedLessThan(Long value) {
addCriterion("modified <", value, "modified");
return (Criteria) this;
}
public Criteria andModifiedLessThanOrEqualTo(Long value) {
addCriterion("modified <=", value, "modified");
return (Criteria) this;
}
public Criteria andModifiedIn(List<Long> values) {
addCriterion("modified in", values, "modified");
return (Criteria) this;
}
public Criteria andModifiedNotIn(List<Long> values) {
addCriterion("modified not in", values, "modified");
return (Criteria) this;
}
public Criteria andModifiedBetween(Long value1, Long value2) {
addCriterion("modified between", value1, value2, "modified");
return (Criteria) this;
}
public Criteria andModifiedNotBetween(Long value1, Long value2) {
addCriterion("modified not between", value1, value2, "modified");
return (Criteria) this;
}
public Criteria andCreatedIsNull() {
addCriterion("created is null");
return (Criteria) this;
}
public Criteria andCreatedIsNotNull() {
addCriterion("created is not null");
return (Criteria) this;
}
public Criteria andCreatedEqualTo(Long value) {
addCriterion("created =", value, "created");
return (Criteria) this;
}
public Criteria andCreatedNotEqualTo(Long value) {
addCriterion("created <>", value, "created");
return (Criteria) this;
}
public Criteria andCreatedGreaterThan(Long value) {
addCriterion("created >", value, "created");
return (Criteria) this;
}
public Criteria andCreatedGreaterThanOrEqualTo(Long value) {
addCriterion("created >=", value, "created");
return (Criteria) this;
}
public Criteria andCreatedLessThan(Long value) {
addCriterion("created <", value, "created");
return (Criteria) this;
}
public Criteria andCreatedLessThanOrEqualTo(Long value) {
addCriterion("created <=", value, "created");
return (Criteria) this;
}
public Criteria andCreatedIn(List<Long> values) {
addCriterion("created in", values, "created");
return (Criteria) this;
}
public Criteria andCreatedNotIn(List<Long> values) {
addCriterion("created not in", values, "created");
return (Criteria) this;
}
public Criteria andCreatedBetween(Long value1, Long value2) {
addCriterion("created between", value1, value2, "created");
return (Criteria) this;
}
public Criteria andCreatedNotBetween(Long value1, Long value2) {
addCriterion("created not between", value1, value2, "created");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {
protected Criteria() {
super();
}
}
public static class Criterion {
private String condition;
private Object value;
private Object secondValue;
private boolean noValue;
private boolean singleValue;
private boolean betweenValue;
private boolean listValue;
private String typeHandler;
public String getCondition() {
return condition;
}
public Object getValue() {
return value;
}
public Object getSecondValue() {
return secondValue;
}
public boolean isNoValue() {
return noValue;
}
public boolean isSingleValue() {
return singleValue;
}
public boolean isBetweenValue() {
return betweenValue;
}
public boolean isListValue() {
return listValue;
}
public String getTypeHandler() {
return typeHandler;
}
protected Criterion(String condition) {
super();
this.condition = condition;
this.typeHandler = null;
this.noValue = true;
}
protected Criterion(String condition, Object value, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.typeHandler = typeHandler;
if (value instanceof List<?>) {
this.listValue = true;
} else {
this.singleValue = true;
}
}
protected Criterion(String condition, Object value) {
this(condition, value, null);
}
protected Criterion(String condition, Object value, Object secondValue, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.secondValue = secondValue;
this.typeHandler = typeHandler;
this.betweenValue = true;
}
protected Criterion(String condition, Object value, Object secondValue) {
this(condition, value, secondValue, null);
}
}
}

View File

@ -115,6 +115,21 @@ CREATE TABLE IF NOT EXISTS project_parameters(
CREATE INDEX idx_project_id ON project_parameters(project_id);
DROP TABLE IF EXISTS worker_node;
CREATE TABLE worker_node
(
id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'auto increment id',
host_name VARCHAR(64) NOT NULL COMMENT 'host name',
port VARCHAR(64) NOT NULL COMMENT 'port',
type INT NOT NULL COMMENT 'node type: ACTUAL or CONTAINER',
launch_date BIGINT NOT NULL COMMENT 'launch date',
modified BIGINT NOT NULL COMMENT 'modified time',
created BIGINT NOT NULL COMMENT 'created time',
PRIMARY KEY(ID)
)
ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT = 'DB WorkerID Assigner for UID Generator';
-- set innodb lock wait timeout to default
SET SESSION innodb_lock_wait_timeout = DEFAULT;

View File

@ -0,0 +1,7 @@
package io.metersphere.sdk.mapper;
import io.metersphere.sdk.domain.WorkerNode;
public interface BaseWorkerNodeMapper {
int insert(WorkerNode record);
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.metersphere.sdk.mapper.BaseWorkerNodeMapper">
<insert id="insert" parameterType="io.metersphere.sdk.domain.WorkerNode" useGeneratedKeys="true" keyProperty="id">
insert into worker_node (host_name, port, type, launch_date, modified, created)
values (#{hostName,jdbcType=VARCHAR}, #{port,jdbcType=VARCHAR}, #{type,jdbcType=INTEGER}, #{launchDate,jdbcType=BIGINT}, #{modified,jdbcType=BIGINT}, #{created,jdbcType=BIGINT})
</insert>
</mapper>

View File

@ -0,0 +1,115 @@
package io.metersphere.sdk.uid;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.springframework.util.Assert;
/**
* Allocate 64 bits for the UID(long)<br>
* sign (fixed 1bit) -> deltaSecond -> workerId -> sequence(within the same second)
**/
public class BitsAllocator {
/**
* Total 64 bits
*/
public static final int TOTAL_BITS = 1 << 6;
/**
* Bits for [sign-> second-> workId-> sequence]
*/
private int signBits = 1;
private final int timestampBits;
private final int workerIdBits;
private final int sequenceBits;
/**
* Max value for workId & sequence
*/
private final long maxDeltaSeconds;
private final long maxWorkerId;
private final long maxSequence;
/**
* Shift for timestamp & workerId
*/
private final int timestampShift;
private final int workerIdShift;
/**
* Constructor with timestampBits, workerIdBits, sequenceBits<br>
* The highest bit used for sign, so <code>63</code> bits for timestampBits, workerIdBits, sequenceBits
*/
public BitsAllocator(int timestampBits, int workerIdBits, int sequenceBits) {
// make sure allocated 64 bits
int allocateTotalBits = signBits + timestampBits + workerIdBits + sequenceBits;
Assert.isTrue(allocateTotalBits == TOTAL_BITS, "allocate not enough 64 bits");
// initialize bits
this.timestampBits = timestampBits;
this.workerIdBits = workerIdBits;
this.sequenceBits = sequenceBits;
// initialize max value
this.maxDeltaSeconds = ~(-1L << timestampBits);
this.maxWorkerId = ~(-1L << workerIdBits);
this.maxSequence = ~(-1L << sequenceBits);
// initialize shift
this.timestampShift = workerIdBits + sequenceBits;
this.workerIdShift = sequenceBits;
}
/**
* Allocate bits for UID according to delta seconds & workerId & sequence<br>
* <b>Note that: </b>The highest bit will always be 0 for sign
*/
public long allocate(long deltaSeconds, long workerId, long sequence) {
return (deltaSeconds << timestampShift) | (workerId << workerIdShift) | sequence;
}
/**
* Getters
*/
public int getSignBits() {
return signBits;
}
public int getTimestampBits() {
return timestampBits;
}
public int getWorkerIdBits() {
return workerIdBits;
}
public int getSequenceBits() {
return sequenceBits;
}
public long getMaxDeltaSeconds() {
return maxDeltaSeconds;
}
public long getMaxWorkerId() {
return maxWorkerId;
}
public long getMaxSequence() {
return maxSequence;
}
public int getTimestampShift() {
return timestampShift;
}
public int getWorkerIdShift() {
return workerIdShift;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}

View File

@ -0,0 +1,26 @@
package io.metersphere.sdk.uid;
import io.metersphere.sdk.uid.impl.CachedUidGenerator;
import io.metersphere.sdk.util.CommonBeanFactory;
public class UUID {
private static final CachedUidGenerator DEFAULT_UID_GENERATOR;
static {
DEFAULT_UID_GENERATOR = CommonBeanFactory.getBean(CachedUidGenerator.class);
}
/**
* 生成一个唯一的数字
*/
public static Long randomUUID() {
return DEFAULT_UID_GENERATOR.getUID();
}
/**
* 生成一个唯一的字符串
*/
public static String nextStr() {
return String.valueOf(DEFAULT_UID_GENERATOR.getUID());
}
}

View File

@ -0,0 +1,168 @@
package io.metersphere.sdk.uid.buffer;
import io.metersphere.sdk.uid.utils.NamingThreadFactory;
import io.metersphere.sdk.uid.utils.PaddedAtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Represents an executor for padding {@link RingBuffer}<br>
* There are two kinds of executors: one for scheduled padding, the other for padding immediately.
*/
public class BufferPaddingExecutor {
private static final Logger LOGGER = LoggerFactory.getLogger(RingBuffer.class);
/**
* Constants
*/
private static final String WORKER_NAME = "RingBuffer-Padding-Worker";
private static final String SCHEDULE_NAME = "RingBuffer-Padding-Schedule";
private static final long DEFAULT_SCHEDULE_INTERVAL = 5 * 60L; // 5 minutes
/**
* Whether buffer padding is running
*/
private final AtomicBoolean running;
/**
* We can borrow UIDs from the future, here store the last second we have consumed
*/
private final PaddedAtomicLong lastSecond;
/**
* RingBuffer & BufferUidProvider
*/
private final RingBuffer ringBuffer;
private final BufferedUidProvider uidProvider;
/**
* Padding immediately by the thread pool
*/
private final ExecutorService bufferPadExecutors;
/**
* Padding schedule thread
*/
private final ScheduledExecutorService bufferPadSchedule;
/**
* Schedule interval Unit as seconds
*/
private long scheduleInterval = DEFAULT_SCHEDULE_INTERVAL;
/**
* Constructor with {@link RingBuffer} and {@link BufferedUidProvider}, default use schedule
*
* @param ringBuffer {@link RingBuffer}
* @param uidProvider {@link BufferedUidProvider}
*/
public BufferPaddingExecutor(RingBuffer ringBuffer, BufferedUidProvider uidProvider) {
this(ringBuffer, uidProvider, true);
}
/**
* Constructor with {@link RingBuffer}, {@link BufferedUidProvider}, and whether use schedule padding
*
* @param ringBuffer {@link RingBuffer}
* @param uidProvider {@link BufferedUidProvider}
*/
public BufferPaddingExecutor(RingBuffer ringBuffer, BufferedUidProvider uidProvider, boolean usingSchedule) {
this.running = new AtomicBoolean(false);
this.lastSecond = new PaddedAtomicLong(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
this.ringBuffer = ringBuffer;
this.uidProvider = uidProvider;
// initialize thread pool
int cores = Runtime.getRuntime().availableProcessors();
bufferPadExecutors = Executors.newFixedThreadPool(cores * 2, new NamingThreadFactory(WORKER_NAME));
// initialize schedule thread
if (usingSchedule) {
bufferPadSchedule = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(SCHEDULE_NAME));
} else {
bufferPadSchedule = null;
}
}
/**
* Start executors such as schedule
*/
public void start() {
if (bufferPadSchedule != null) {
bufferPadSchedule.scheduleWithFixedDelay(this::paddingBuffer, scheduleInterval, scheduleInterval, TimeUnit.SECONDS);
}
}
/**
* Shutdown executors
*/
public void shutdown() {
if (!bufferPadExecutors.isShutdown()) {
bufferPadExecutors.shutdownNow();
}
if (bufferPadSchedule != null && !bufferPadSchedule.isShutdown()) {
bufferPadSchedule.shutdownNow();
}
}
/**
* Whether is padding
*/
public boolean isRunning() {
return running.get();
}
/**
* Padding buffer in the thread pool
*/
public void asyncPadding() {
bufferPadExecutors.submit(this::paddingBuffer);
}
/**
* Padding buffer fill the slots until to catch the cursor
*/
public void paddingBuffer() {
LOGGER.info("Ready to padding buffer lastSecond:{}. {}", lastSecond.get(), ringBuffer);
// is still running
if (!running.compareAndSet(false, true)) {
LOGGER.info("Padding buffer is still running. {}", ringBuffer);
return;
}
// fill the rest slots until to catch the cursor
boolean isFullRingBuffer = false;
while (!isFullRingBuffer) {
List<Long> uidList = uidProvider.provide(lastSecond.incrementAndGet());
for (Long uid : uidList) {
isFullRingBuffer = !ringBuffer.put(uid);
if (isFullRingBuffer) {
break;
}
}
}
// not running now
running.compareAndSet(true, false);
LOGGER.info("End to padding buffer lastSecond:{}. {}", lastSecond.get(), ringBuffer);
}
/**
* Setters
*/
public void setScheduleInterval(long scheduleInterval) {
Assert.isTrue(scheduleInterval > 0, "Schedule interval must positive!");
this.scheduleInterval = scheduleInterval;
}
}

View File

@ -0,0 +1,16 @@
package io.metersphere.sdk.uid.buffer;
import java.util.List;
/**
* Buffered UID provider(Lambda supported), which provides UID in the same one second
*/
@FunctionalInterface
public interface BufferedUidProvider {
/**
* Provides UID in one second
*/
List<Long> provide(long momentInSecond);
}

View File

@ -0,0 +1,15 @@
package io.metersphere.sdk.uid.buffer;
/**
* If tail catches the cursor it means that the ring buffer is full, any more buffer put request will be rejected.
* Specify the policy to handle the reject. This is a Lambda supported interface
*/
@FunctionalInterface
public interface RejectedPutBufferHandler {
/**
* Reject put buffer request
*/
void rejectPutBuffer(RingBuffer ringBuffer, long uid);
}

View File

@ -0,0 +1,15 @@
package io.metersphere.sdk.uid.buffer;
/**
* If cursor catches the tail it means that the ring buffer is empty, any more buffer take request will be rejected.
* Specify the policy to handle the reject. This is a Lambda supported interface
*/
@FunctionalInterface
public interface RejectedTakeBufferHandler {
/**
* Reject take buffer request
*/
void rejectTakeBuffer(RingBuffer ringBuffer);
}

View File

@ -0,0 +1,251 @@
package io.metersphere.sdk.uid.buffer;
import io.metersphere.sdk.uid.utils.PaddedAtomicLong;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import java.util.concurrent.atomic.AtomicLong;
/**
* Represents a ring buffer based on array.<br>
* Using array could improve read element performance due to the CUP cache line. To prevent
* the side effect of False Sharing, {@link PaddedAtomicLong} is using on 'tail' and 'cursor'<p>
* <p>
* A ring buffer is consisted of:
* <li><b>slots:</b> each element of the array is a slot, which is be set with a UID
* <li><b>flags:</b> flag array corresponding the same index with the slots, indicates whether can take or put slot
* <li><b>tail:</b> a sequence of the max slot position to produce
* <li><b>cursor:</b> a sequence of the min slot position to consume
*/
public class RingBuffer {
private static final Logger LOGGER = LoggerFactory.getLogger(RingBuffer.class);
/**
* Constants
*/
private static final int START_POINT = -1;
private static final long CAN_PUT_FLAG = 0L;
private static final long CAN_TAKE_FLAG = 1L;
public static final int DEFAULT_PADDING_PERCENT = 50;
/**
* The size of RingBuffer's slots, each slot hold a UID
*/
@Getter
private final int bufferSize;
private final long indexMask;
private final long[] slots;
private final PaddedAtomicLong[] flags;
/**
* Tail: last position sequence to produce
*/
private final AtomicLong tail = new PaddedAtomicLong(START_POINT);
/**
* Cursor: current position sequence to consume
*/
private final AtomicLong cursor = new PaddedAtomicLong(START_POINT);
/**
* Threshold for trigger padding buffer
*/
private final int paddingThreshold;
/**
* Reject put/take buffer handle policy
*/
private RejectedPutBufferHandler rejectedPutHandler = this::discardPutBuffer;
private RejectedTakeBufferHandler rejectedTakeHandler = this::exceptionRejectedTakeBuffer;
/**
* Executor of padding buffer
*/
private BufferPaddingExecutor bufferPaddingExecutor;
/**
* Constructor with buffer size, paddingFactor default as {@value #DEFAULT_PADDING_PERCENT}
*
* @param bufferSize must be positive & a power of 2
*/
public RingBuffer(int bufferSize) {
this(bufferSize, DEFAULT_PADDING_PERCENT);
}
/**
* Constructor with buffer size & padding factor
*
* @param bufferSize must be positive & a power of 2
* @param paddingFactor percent in (0 - 100). When the count of rest available UIDs reach the threshold, it will trigger padding buffer<br>
* Sample: paddingFactor=20, bufferSize=1000 -> threshold=1000 * 20 /100,
* padding buffer will be triggered when tail-cursor<threshold
*/
public RingBuffer(int bufferSize, int paddingFactor) {
// check buffer size is positive & a power of 2; padding factor in (0, 100)
Assert.isTrue(bufferSize > 0L, "RingBuffer size must be positive");
Assert.isTrue(Integer.bitCount(bufferSize) == 1, "RingBuffer size must be a power of 2");
Assert.isTrue(paddingFactor > 0 && paddingFactor < 100, "RingBuffer size must be positive");
this.bufferSize = bufferSize;
this.indexMask = bufferSize - 1;
this.slots = new long[bufferSize];
this.flags = initFlags(bufferSize);
this.paddingThreshold = bufferSize * paddingFactor / 100;
}
/**
* Put an UID in the ring & tail moved<br>
* We use 'synchronized' to guarantee the UID fill in slot & publish new tail sequence as atomic operations<br>
*
* <b>Note that: </b> It is recommended to put UID in a serialize way, cause we once batch generate a series UIDs and put
* the one by one into the buffer, so it is unnecessary put in multi-threads
* * @return false means that the buffer is full, apply {@link RejectedPutBufferHandler}
*/
public synchronized boolean put(long uid) {
long currentTail = tail.get();
long currentCursor = cursor.get();
// tail catches the cursor, means that you can't put any cause of RingBuffer is full
long distance = currentTail - (currentCursor == START_POINT ? 0 : currentCursor);
if (distance == bufferSize - 1) {
rejectedPutHandler.rejectPutBuffer(this, uid);
return false;
}
// 1. pre-check whether the flag is CAN_PUT_FLAG
int nextTailIndex = calSlotIndex(currentTail + 1);
if (flags[nextTailIndex].get() != CAN_PUT_FLAG) {
rejectedPutHandler.rejectPutBuffer(this, uid);
return false;
}
// 2. put UID in the next slot
// 3. update next slot' flag to CAN_TAKE_FLAG
// 4. publish tail with sequence increase by one
slots[nextTailIndex] = uid;
flags[nextTailIndex].set(CAN_TAKE_FLAG);
tail.incrementAndGet();
// The atomicity of operations above, guarantees by 'synchronized'. In another word,
// the take operation can't consume the UID we just put, until the tail is published(tail.incrementAndGet())
return true;
}
/**
* Take an UID of the ring at the next cursor, this is a lock free operation by using atomic cursor<p>
* <p>
* Before getting the UID, we also check whether reach the padding threshold,
* the padding buffer operation will be triggered in another thread<br>
* If there is no more available UID to be taken, the specified {@link RejectedTakeBufferHandler} will be applied<br>
*
* @return UID
* @throws IllegalStateException if the cursor moved back
*/
public long take() {
// spin get next available cursor
long currentCursor = cursor.get();
long nextCursor = cursor.updateAndGet(old -> old == tail.get() ? old : old + 1);
// check for safety consideration, it never occurs
Assert.isTrue(nextCursor >= currentCursor, "Curosr can't move back");
// trigger padding in an async-mode if reach the threshold
long currentTail = tail.get();
if (currentTail - nextCursor < paddingThreshold) {
LOGGER.info("Reach the padding threshold:{}. tail:{}, cursor:{}, rest:{}", paddingThreshold, currentTail,
nextCursor, currentTail - nextCursor);
bufferPaddingExecutor.asyncPadding();
}
// cursor catch the tail, means that there is no more available UID to take
if (nextCursor == currentCursor) {
rejectedTakeHandler.rejectTakeBuffer(this);
}
// 1. check next slot flag is CAN_TAKE_FLAG
int nextCursorIndex = calSlotIndex(nextCursor);
Assert.isTrue(flags[nextCursorIndex].get() == CAN_TAKE_FLAG, "Curosr not in can take status");
// 2. get UID from next slot
// 3. set next slot flag as CAN_PUT_FLAG.
long uid = slots[nextCursorIndex];
flags[nextCursorIndex].set(CAN_PUT_FLAG);
// Note that: Step 2,3 can not swap. If we set flag before get value of slot, the producer may overwrite the
// slot with a new UID, and this may cause the consumer take the UID twice after walk a round the ring
return uid;
}
/**
* Calculate slot index with the slot sequence (sequence % bufferSize)
*/
protected int calSlotIndex(long sequence) {
return (int) (sequence & indexMask);
}
/**
* Discard policy for {@link RejectedPutBufferHandler}, we just do logging
*/
protected void discardPutBuffer(RingBuffer ringBuffer, long uid) {
LOGGER.warn("Rejected putting buffer for uid:{}. {}", uid, ringBuffer);
}
/**
* Policy for {@link RejectedTakeBufferHandler}, throws {@link RuntimeException} after logging
*/
protected void exceptionRejectedTakeBuffer(RingBuffer ringBuffer) {
LOGGER.warn("Rejected take buffer. {}", ringBuffer);
throw new RuntimeException("Rejected take buffer. " + ringBuffer);
}
/**
* Initialize flags as CAN_PUT_FLAG
*/
private PaddedAtomicLong[] initFlags(int bufferSize) {
PaddedAtomicLong[] flags = new PaddedAtomicLong[bufferSize];
for (int i = 0; i < bufferSize; i++) {
flags[i] = new PaddedAtomicLong(CAN_PUT_FLAG);
}
return flags;
}
/**
* Getters
*/
public long getTail() {
return tail.get();
}
public long getCursor() {
return cursor.get();
}
/**
* Setters
*/
public void setBufferPaddingExecutor(BufferPaddingExecutor bufferPaddingExecutor) {
this.bufferPaddingExecutor = bufferPaddingExecutor;
}
public void setRejectedPutHandler(RejectedPutBufferHandler rejectedPutHandler) {
this.rejectedPutHandler = rejectedPutHandler;
}
public void setRejectedTakeHandler(RejectedTakeBufferHandler rejectedTakeHandler) {
this.rejectedTakeHandler = rejectedTakeHandler;
}
@Override
public String toString() {
return "RingBuffer [bufferSize=" + bufferSize +
", tail=" + tail +
", cursor=" + cursor +
", paddingThreshold=" + paddingThreshold + "]";
}
}

View File

@ -0,0 +1,147 @@
package io.metersphere.sdk.uid.impl;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.uid.BitsAllocator;
import io.metersphere.sdk.uid.buffer.BufferPaddingExecutor;
import io.metersphere.sdk.uid.buffer.RejectedPutBufferHandler;
import io.metersphere.sdk.uid.buffer.RejectedTakeBufferHandler;
import io.metersphere.sdk.uid.buffer.RingBuffer;
import io.metersphere.sdk.util.LogUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import java.util.ArrayList;
import java.util.List;
/**
* from {@link DefaultUidGenerator}, based on a lock free {@link RingBuffer}<p>
*/
@Service
public class CachedUidGenerator extends DefaultUidGenerator implements DisposableBean, InitializingBean {
private static final int DEFAULT_BOOST_POWER = 3;
/**
* Spring properties
*/
private int boostPower = DEFAULT_BOOST_POWER;
private Long scheduleInterval;
private RejectedPutBufferHandler rejectedPutBufferHandler;
private RejectedTakeBufferHandler rejectedTakeBufferHandler;
/**
* RingBuffer
*/
private RingBuffer ringBuffer;
private BufferPaddingExecutor bufferPaddingExecutor;
@Override
public void afterPropertiesSet() {
// initialize workerId & bitsAllocator
super.afterPropertiesSet();
// initialize RingBuffer & RingBufferPaddingExecutor
this.initRingBuffer();
LogUtils.info("Initialized RingBuffer successfully.");
}
@Override
public long getUID() {
try {
return ringBuffer.take();
} catch (Exception e) {
LogUtils.error("Generate unique id exception. ", e);
throw new MSException(e);
}
}
@Override
public String parseUID(long uid) {
return super.parseUID(uid);
}
@Override
public void destroy() throws Exception {
bufferPaddingExecutor.shutdown();
}
/**
* Get the UIDs in the same specified second under the max sequence
* * @return UID list, size of {@link BitsAllocator#getMaxSequence()} + 1
*/
protected List<Long> nextIdsForOneSecond(long currentSecond) {
// Initialize result list size of (max sequence + 1)
int listSize = (int) bitsAllocator.getMaxSequence() + 1;
List<Long> uidList = new ArrayList<>(listSize);
// Allocate the first sequence of the second, the others can be calculated with the offset
long firstSeqUid = bitsAllocator.allocate(currentSecond - epochSeconds, workerId, 0L);
for (int offset = 0; offset < listSize; offset++) {
uidList.add(firstSeqUid + offset);
}
return uidList;
}
/**
* Initialize RingBuffer & RingBufferPaddingExecutor
*/
private void initRingBuffer() {
// initialize RingBuffer
int bufferSize = ((int) bitsAllocator.getMaxSequence() + 1) << boostPower;
int paddingFactor = RingBuffer.DEFAULT_PADDING_PERCENT;
this.ringBuffer = new RingBuffer(bufferSize, paddingFactor);
LogUtils.info("Initialized ring buffer size:{}, paddingFactor:{}", bufferSize, paddingFactor);
// initialize RingBufferPaddingExecutor
boolean usingSchedule = (scheduleInterval != null);
this.bufferPaddingExecutor = new BufferPaddingExecutor(ringBuffer, this::nextIdsForOneSecond, usingSchedule);
if (usingSchedule) {
bufferPaddingExecutor.setScheduleInterval(scheduleInterval);
}
LogUtils.info("Initialized BufferPaddingExecutor. Using schdule:{}, interval:{}", usingSchedule, scheduleInterval);
// set rejected put/take handle policy
this.ringBuffer.setBufferPaddingExecutor(bufferPaddingExecutor);
if (rejectedPutBufferHandler != null) {
this.ringBuffer.setRejectedPutHandler(rejectedPutBufferHandler);
}
if (rejectedTakeBufferHandler != null) {
this.ringBuffer.setRejectedTakeHandler(rejectedTakeBufferHandler);
}
// fill in all slots of the RingBuffer
bufferPaddingExecutor.paddingBuffer();
// start buffer padding threads
bufferPaddingExecutor.start();
}
/**
* Setters for spring property
*/
public void setBoostPower(int boostPower) {
Assert.isTrue(boostPower > 0, "Boost power must be positive!");
this.boostPower = boostPower;
}
public void setRejectedPutBufferHandler(RejectedPutBufferHandler rejectedPutBufferHandler) {
Assert.notNull(rejectedPutBufferHandler, "RejectedPutBufferHandler can't be null!");
this.rejectedPutBufferHandler = rejectedPutBufferHandler;
}
public void setRejectedTakeBufferHandler(RejectedTakeBufferHandler rejectedTakeBufferHandler) {
Assert.notNull(rejectedTakeBufferHandler, "RejectedTakeBufferHandler can't be null!");
this.rejectedTakeBufferHandler = rejectedTakeBufferHandler;
}
public void setScheduleInterval(long scheduleInterval) {
Assert.isTrue(scheduleInterval > 0, "Schedule interval must positive!");
this.scheduleInterval = scheduleInterval;
}
}

View File

@ -0,0 +1,179 @@
package io.metersphere.sdk.uid.impl;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.uid.BitsAllocator;
import io.metersphere.sdk.uid.utils.TimeUtils;
import io.metersphere.sdk.uid.worker.WorkerIdAssigner;
import io.metersphere.sdk.util.LogUtils;
import jakarta.annotation.Resource;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Service
public class DefaultUidGenerator {
/**
* Bits allocate
*/
protected int timeBits = 28;
protected int workerBits = 22;
protected int seqBits = 13;
/**
* Customer epoch, unit as second. For example 2023-09-01 (ms: 1693548784)
*/
protected String epochStr = "2023-09-01";
protected long epochSeconds = TimeUnit.MILLISECONDS.toSeconds(1693548784);
/**
* Stable fields after spring bean initializing
*/
protected BitsAllocator bitsAllocator;
protected long workerId;
/**
* Volatile fields caused by nextId()
*/
protected long sequence = 0L;
protected long lastSecond = -1L;
/**
* Spring property
*/
@Resource
protected WorkerIdAssigner workerIdAssigner;
public void afterPropertiesSet() {
// init bitsAllocator
this.setTimeBits(29);
this.setWorkerBits(21);
this.setSeqBits(13);
this.setEpochStr(TimeUtils.getDataStr(System.currentTimeMillis()));
// initialize bits allocator
bitsAllocator = new BitsAllocator(timeBits, workerBits, seqBits);
// initialize worker id
workerId = workerIdAssigner.assignWorkerId();
if (workerId > bitsAllocator.getMaxWorkerId()) {
throw new RuntimeException("Worker id " + workerId + " exceeds the max " + bitsAllocator.getMaxWorkerId());
}
LogUtils.info("Initialized bits(1, {}, {}, {}) for workerID:{}", timeBits, workerBits, seqBits, workerId);
}
public long getUID() throws MSException {
try {
return nextId();
} catch (Exception e) {
LogUtils.error("Generate unique id exception. ", e);
throw new RuntimeException(e);
}
}
public String parseUID(long uid) {
long totalBits = BitsAllocator.TOTAL_BITS;
long signBits = bitsAllocator.getSignBits();
long timestampBits = bitsAllocator.getTimestampBits();
long workerIdBits = bitsAllocator.getWorkerIdBits();
long sequenceBits = bitsAllocator.getSequenceBits();
// parse UID
long sequence = (uid << (totalBits - sequenceBits)) >>> (totalBits - sequenceBits);
long workerId = (uid << (timestampBits + signBits)) >>> (totalBits - workerIdBits);
long deltaSeconds = uid >>> (workerIdBits + sequenceBits);
Date thatTime = new Date(TimeUnit.SECONDS.toMillis(epochSeconds + deltaSeconds));
String thatTimeStr = TimeUtils.formatByDateTimePattern(thatTime);
// format as string
return String.format("{\"UID\":\"%d\",\"timestamp\":\"%s\",\"workerId\":\"%d\",\"sequence\":\"%d\"}",
uid, thatTimeStr, workerId, sequence);
}
/**
* Get UID
*
* @return UID
* @throws MSException in the case: Clock moved backwards; Exceeds the max timestamp
*/
protected synchronized long nextId() {
long currentSecond = getCurrentSecond();
// Clock moved backwards, refuse to generate uid
if (currentSecond < lastSecond) {
long refusedSeconds = lastSecond - currentSecond;
throw new MSException("Clock moved backwards. Refusing for %d seconds" + refusedSeconds);
}
// At the same second, increase sequence
if (currentSecond == lastSecond) {
sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
// Exceed the max sequence, we wait the next second to generate uid
if (sequence == 0) {
currentSecond = getNextSecond(lastSecond);
}
// At the different second, sequence restart from zero
} else {
sequence = 0L;
}
lastSecond = currentSecond;
// Allocate bits for UID
return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
}
/**
* Get next millisecond
*/
private long getNextSecond(long lastTimestamp) {
long timestamp = getCurrentSecond();
while (timestamp <= lastTimestamp) {
timestamp = getCurrentSecond();
}
return timestamp;
}
/**
* Get current second
*/
private long getCurrentSecond() {
long currentSecond = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
if (currentSecond - epochSeconds > bitsAllocator.getMaxDeltaSeconds()) {
throw new MSException("Timestamp bits is exhausted. Refusing UID generate. Now: " + currentSecond);
}
return currentSecond;
}
public void setTimeBits(int timeBits) {
if (timeBits > 0) {
this.timeBits = timeBits;
}
}
public void setWorkerBits(int workerBits) {
if (workerBits > 0) {
this.workerBits = workerBits;
}
}
public void setSeqBits(int seqBits) {
if (seqBits > 0) {
this.seqBits = seqBits;
}
}
public void setEpochStr(String epochStr) {
if (StringUtils.isNotBlank(epochStr)) {
this.epochStr = epochStr;
this.epochSeconds = TimeUnit.MILLISECONDS.toSeconds(TimeUtils.parseByDayPattern(epochStr).getTime());
}
}
}

View File

@ -0,0 +1,90 @@
package io.metersphere.sdk.uid.utils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DockerUtils
*
*/
public abstract class DockerUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(DockerUtils.class);
/** Environment param keys */
private static final String ENV_KEY_HOST = "JPAAS_HOST";
private static final String ENV_KEY_PORT = "JPAAS_HTTP_PORT";
private static final String ENV_KEY_PORT_ORIGINAL = "JPAAS_HOST_PORT_8080";
/** Docker host & port */
private static String DOCKER_HOST = "";
private static String DOCKER_PORT = "";
/** Whether is docker */
private static boolean IS_DOCKER;
static {
retrieveFromEnv();
}
/**
* Retrieve docker host
*
* @return empty string if not a docker
*/
public static String getDockerHost() {
return DOCKER_HOST;
}
/**
* Retrieve docker port
*
* @return empty string if not a docker
*/
public static String getDockerPort() {
return DOCKER_PORT;
}
/**
* Whether a docker
*
* @return
*/
public static boolean isDocker() {
return IS_DOCKER;
}
/**
* Retrieve host & port from environment
*/
private static void retrieveFromEnv() {
// retrieve host & port from environment
DOCKER_HOST = System.getenv(ENV_KEY_HOST);
DOCKER_PORT = System.getenv(ENV_KEY_PORT);
// not found from 'JPAAS_HTTP_PORT', then try to find from 'JPAAS_HOST_PORT_8080'
if (StringUtils.isBlank(DOCKER_PORT)) {
DOCKER_PORT = System.getenv(ENV_KEY_PORT_ORIGINAL);
}
boolean hasEnvHost = StringUtils.isNotBlank(DOCKER_HOST);
boolean hasEnvPort = StringUtils.isNotBlank(DOCKER_PORT);
// docker can find both host & port from environment
if (hasEnvHost && hasEnvPort) {
IS_DOCKER = true;
// found nothing means not a docker, maybe an actual machine
} else if (!hasEnvHost && !hasEnvPort) {
IS_DOCKER = false;
} else {
LOGGER.error("Missing host or port from env for Docker. host:{}, port:{}", DOCKER_HOST, DOCKER_PORT);
throw new RuntimeException(
"Missing host or port from env for Docker. host:" + DOCKER_HOST + ", port:" + DOCKER_PORT);
}
}
}

View File

@ -0,0 +1,39 @@
package io.metersphere.sdk.uid.utils;
import org.springframework.util.Assert;
/**
* EnumUtils provides the operations for {@link ValuedEnum} such as Parse, value of...
*/
public abstract class EnumUtils {
/**
* Parse the bounded value into ValuedEnum
*/
public static <T extends ValuedEnum<V>, V> T parse(Class<T> clz, V value) {
Assert.notNull(clz, "clz can not be null");
if (value == null) {
return null;
}
for (T t : clz.getEnumConstants()) {
if (value.equals(t.value())) {
return t;
}
}
return null;
}
/**
* Null-safe valueOf function
*/
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
if (name == null) {
return null;
}
return Enum.valueOf(enumType, name);
}
}

View File

@ -0,0 +1,148 @@
package io.metersphere.sdk.uid.utils;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong;
/**
* Named thread in ThreadFactory. If there is no specified name for thread, it
* will auto detect using the invoker classname instead.
*/
public class NamingThreadFactory implements ThreadFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(NamingThreadFactory.class);
/**
* Thread name pre
*/
private String name;
/**
* Is daemon thread
*/
private boolean daemon;
/**
* UncaughtExceptionHandler
*/
private UncaughtExceptionHandler uncaughtExceptionHandler;
/**
* Sequences for multi thread name prefix
*/
private final ConcurrentHashMap<String, AtomicLong> sequences;
/**
* Constructors
*/
public NamingThreadFactory() {
this(null, false, null);
}
public NamingThreadFactory(String name) {
this(name, false, null);
}
public NamingThreadFactory(String name, boolean daemon) {
this(name, daemon, null);
}
public NamingThreadFactory(String name, boolean daemon, UncaughtExceptionHandler handler) {
this.name = name;
this.daemon = daemon;
this.uncaughtExceptionHandler = handler;
this.sequences = new ConcurrentHashMap<String, AtomicLong>();
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(this.daemon);
// If there is no specified name for thread, it will auto detect using the invoker classname instead.
// Notice that auto detect may cause some performance overhead
String prefix = this.name;
if (StringUtils.isBlank(prefix)) {
prefix = getInvoker(2);
}
thread.setName(prefix + "-" + getSequence(prefix));
// no specified uncaughtExceptionHandler, just do logging.
if (this.uncaughtExceptionHandler != null) {
thread.setUncaughtExceptionHandler(this.uncaughtExceptionHandler);
} else {
thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("unhandled exception in thread: " + t.getId() + ":" + t.getName(), e);
}
});
}
return thread;
}
/**
* Get the method invoker's class name
*
* @param depth
* @return
*/
private String getInvoker(int depth) {
Exception e = new Exception();
StackTraceElement[] stes = e.getStackTrace();
if (stes.length > depth) {
return ClassUtils.getShortClassName(stes[depth].getClassName());
}
return getClass().getSimpleName();
}
/**
* Get sequence for different naming prefix
*
* @param invoker
* @return
*/
private long getSequence(String invoker) {
AtomicLong r = this.sequences.get(invoker);
if (r == null) {
r = new AtomicLong(0);
AtomicLong previous = this.sequences.putIfAbsent(invoker, r);
if (previous != null) {
r = previous;
}
}
return r.incrementAndGet();
}
/**
* Getters & Setters
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isDaemon() {
return daemon;
}
public void setDaemon(boolean daemon) {
this.daemon = daemon;
}
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler;
}
public void setUncaughtExceptionHandler(UncaughtExceptionHandler handler) {
this.uncaughtExceptionHandler = handler;
}
}

View File

@ -0,0 +1,70 @@
package io.metersphere.sdk.uid.utils;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
/**
* NetUtils
*
*/
public abstract class NetUtils {
/**
* Pre-loaded local address
*/
public static InetAddress localAddress;
static {
try {
localAddress = getLocalInetAddress();
} catch (SocketException e) {
throw new RuntimeException("fail to get local ip.");
}
}
/**
* Retrieve the first validated local ip address(the Public and LAN ip addresses are validated).
*
* @return the local address
* @throws SocketException the socket exception
*/
public static InetAddress getLocalInetAddress() throws SocketException {
// enumerates all network interfaces
Enumeration<NetworkInterface> enu = NetworkInterface.getNetworkInterfaces();
while (enu.hasMoreElements()) {
NetworkInterface ni = enu.nextElement();
if (ni.isLoopback()) {
continue;
}
Enumeration<InetAddress> addressEnumeration = ni.getInetAddresses();
while (addressEnumeration.hasMoreElements()) {
InetAddress address = addressEnumeration.nextElement();
// ignores all invalidated addresses
if (address.isLinkLocalAddress() || address.isLoopbackAddress() || address.isAnyLocalAddress()) {
continue;
}
return address;
}
}
throw new RuntimeException("No validated local address!");
}
/**
* Retrieve local address
*
* @return the string local address
*/
public static String getLocalAddress() {
return localAddress.getHostAddress();
}
}

View File

@ -0,0 +1,38 @@
package io.metersphere.sdk.uid.utils;
import java.util.concurrent.atomic.AtomicLong;
/**
* Represents a padded {@link AtomicLong} to prevent the FalseSharing problem<p>
*
* The CPU cache line commonly be 64 bytes, here is a sample of cache line after padding:<br>
* 64 bytes = 8 bytes (object reference) + 6 * 8 bytes (padded long) + 8 bytes (a long value)
*
*/
public class PaddedAtomicLong extends AtomicLong {
private static final long serialVersionUID = -3415778863941386253L;
/** Padded 6 long (48 bytes) */
public volatile long p1, p2, p3, p4, p5, p6 = 7L;
/**
* Constructors from {@link AtomicLong}
*/
public PaddedAtomicLong() {
super();
}
public PaddedAtomicLong(long initialValue) {
super(initialValue);
}
/**
* To prevent GC optimizations for cleaning unused padded references
*/
public long sumPaddingToPreventOptimization() {
return p1 + p2 + p3 + p4 + p5 + p6;
}
}

View File

@ -0,0 +1,53 @@
package io.metersphere.sdk.uid.utils;
import org.apache.commons.lang.time.DateFormatUtils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* DateUtils provides date formatting, parsing
*/
public abstract class TimeUtils extends org.apache.commons.lang.time.DateUtils {
/**
* Patterns
*/
public static final String DAY_PATTERN = "yyyy-MM-dd";
public static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* Parse date by 'yyyy-MM-dd' pattern
*/
public static Date parseByDayPattern(String str) {
return parseDate(str, DAY_PATTERN);
}
/**
* Parse date without Checked exception
*
* @throws RuntimeException when ParseException occurred
*/
public static Date parseDate(String str, String pattern) {
try {
return parseDate(str, new String[]{pattern});
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
/**
* Format date by 'yyyy-MM-dd HH:mm:ss' pattern
*/
public static String formatByDateTimePattern(Date date) {
return DateFormatUtils.format(date, DATETIME_PATTERN);
}
public static String getDataStr(long timeStamp) {
SimpleDateFormat dateFormat = new SimpleDateFormat(DAY_PATTERN);
return dateFormat.format(timeStamp);
}
}

View File

@ -0,0 +1,11 @@
package io.metersphere.sdk.uid.utils;
/**
* {@code ValuedEnum} defines an enumeration which is bounded to a value, you
* may implements this interface when you defines such kind of enumeration, that
* you can use {@link EnumUtils} to simplify parse and valueOf operation.
*/
public interface ValuedEnum<T> {
T value();
}

View File

@ -0,0 +1,66 @@
package io.metersphere.sdk.uid.worker;
import io.metersphere.sdk.domain.WorkerNode;
import io.metersphere.sdk.mapper.BaseWorkerNodeMapper;
import io.metersphere.sdk.uid.utils.DockerUtils;
import io.metersphere.sdk.uid.utils.NetUtils;
import io.metersphere.sdk.util.LogUtils;
import jakarta.annotation.Resource;
import org.apache.commons.lang.math.RandomUtils;
import org.springframework.stereotype.Service;
/**
* Represents an implementation of {@link WorkerIdAssigner},
* the worker id will be discarded after assigned to the UidGenerator
*/
@Service
public class DisposableWorkerIdAssigner implements WorkerIdAssigner {
@Resource
private BaseWorkerNodeMapper workerNodeMapper;
/**
* Assign worker id base on database.<p>
* If there is host name & port in the environment, we considered that the node runs in Docker container<br>
* Otherwise, the node runs on an actual machine.
*
* @return assigned worker id
*/
public long assignWorkerId() {
// build worker node entity
try {
WorkerNode workerNode = buildWorkerNode();
// add worker node for new (ignore the same IP + PORT)
workerNodeMapper.insert(workerNode);
LogUtils.info("Add worker node:" + workerNode);
return workerNode.getId();
} catch (Exception e) {
LogUtils.error("Assign worker id exception. ", e);
return 1;
}
}
/**
* Build worker node entity by IP and PORT
*/
private WorkerNode buildWorkerNode() {
WorkerNode workerNode = new WorkerNode();
if (DockerUtils.isDocker()) {
workerNode.setType(WorkerNodeType.CONTAINER.value());
workerNode.setHostName(DockerUtils.getDockerHost());
workerNode.setPort(DockerUtils.getDockerPort());
} else {
workerNode.setType(WorkerNodeType.ACTUAL.value());
workerNode.setHostName(NetUtils.getLocalAddress());
workerNode.setPort(System.currentTimeMillis() + "-" + RandomUtils.nextInt(100000));
}
workerNode.setCreated(System.currentTimeMillis());
workerNode.setModified(System.currentTimeMillis());
workerNode.setLaunchDate(System.currentTimeMillis());
return workerNode;
}
}

View File

@ -0,0 +1,18 @@
package io.metersphere.sdk.uid.worker;
/**
* Represents a worker id assigner for {@link io.metersphere.sdk.uid.impl.DefaultUidGenerator}
*
*/
public interface WorkerIdAssigner {
/**
* Assign worker id for {@link io.metersphere.sdk.uid.impl.DefaultUidGenerator}
*
* @return assigned worker id
*/
long assignWorkerId();
}

View File

@ -0,0 +1,33 @@
package io.metersphere.sdk.uid.worker;
import io.metersphere.sdk.uid.utils.ValuedEnum;
/**
* WorkerNodeType
* <li>CONTAINER: Such as Docker
* <li>ACTUAL: Actual machine
*
*/
public enum WorkerNodeType implements ValuedEnum<Integer> {
CONTAINER(1), ACTUAL(2);
/**
* Lock type
*/
private final Integer type;
/**
* Constructor with field of type
*/
private WorkerNodeType(Integer type) {
this.type = type;
}
@Override
public Integer value() {
return type;
}
}

View File

@ -17,8 +17,6 @@ public class LogUtils {
/**
* 初始化日志
*
* @return
*/
public static Logger getLogger() {
return LoggerFactory.getLogger(LogUtils.getLogClass());
@ -57,6 +55,14 @@ public class LogUtils {
}
}
public static void info(String var1, Object... var2) {
Logger logger = LogUtils.getLogger();
if (logger != null && logger.isInfoEnabled()) {
logger.info(var1, var2);
}
}
public static void info(Object msg, Object o1) {
Logger logger = LogUtils.getLogger();
if (logger != null && logger.isInfoEnabled()) {
@ -197,8 +203,6 @@ public class LogUtils {
/**
* 得到调用类名称
*
* @return
*/
private static String getLogClass() {
StackTraceElement[] stack = (new Throwable()).getStackTrace();
@ -211,8 +215,6 @@ public class LogUtils {
/**
* 得到调用方法名称
*
* @return
*/
private static String getLogMethod() {
StackTraceElement[] stack = (new Throwable()).getStackTrace();