split elastic-job-cloud as a independent repo
diff --git a/elastic-job-cloud-common/pom.xml b/elastic-job-cloud-common/pom.xml
new file mode 100644
index 0000000..5b6ab99
--- /dev/null
+++ b/elastic-job-cloud-common/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>elastic-job-cloud</artifactId>
+ <groupId>io.elasticjob</groupId>
+ <version>3.0.0.M1-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>elastic-job-cloud-common</artifactId>
+ <name>${project.artifactId}</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-framework</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-client</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-recipes</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-test</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.quartz-scheduler</groupId>
+ <artifactId>quartz</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-exec</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.unitils</groupId>
+ <artifactId>unitils-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-inline</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>jcl-over-slf4j</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>log4j-over-slf4j</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-dbcp</groupId>
+ <artifactId>commons-dbcp</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ </dependency>
+ </dependencies>
+</project>
\ No newline at end of file
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/ElasticJob.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/ElasticJob.java
new file mode 100644
index 0000000..0906215
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/ElasticJob.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.api;
+
+/**
+ * 弹性化分布式作业标识接口.
+ *
+ * @author zhangliang
+ */
+public interface ElasticJob {
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/JobType.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/JobType.java
new file mode 100644
index 0000000..fe9c7ba
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/JobType.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.api;
+
+/**
+ * 作业类型.
+ *
+ * @author caohao
+ */
+public enum JobType {
+
+ SIMPLE, DATAFLOW, SCRIPT
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/ShardingContext.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/ShardingContext.java
new file mode 100644
index 0000000..63dacf9
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/ShardingContext.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.api;
+
+import io.elasticjob.cloud.executor.ShardingContexts;
+import lombok.Getter;
+import lombok.ToString;
+
+/**
+ * 分片上下文.
+ *
+ * @author zhangliang
+ */
+@Getter
+@ToString
+public final class ShardingContext {
+
+ /**
+ * 作业名称.
+ */
+ private final String jobName;
+
+ /**
+ * 作业任务ID.
+ */
+ private final String taskId;
+
+ /**
+ * 分片总数.
+ */
+ private final int shardingTotalCount;
+
+ /**
+ * 作业自定义参数.
+ * 可以配置多个相同的作业, 但是用不同的参数作为不同的调度实例.
+ */
+ private final String jobParameter;
+
+ /**
+ * 分配于本作业实例的分片项.
+ */
+ private final int shardingItem;
+
+ /**
+ * 分配于本作业实例的分片参数.
+ */
+ private final String shardingParameter;
+
+ public ShardingContext(final ShardingContexts shardingContexts, final int shardingItem) {
+ jobName = shardingContexts.getJobName();
+ taskId = shardingContexts.getTaskId();
+ shardingTotalCount = shardingContexts.getShardingTotalCount();
+ jobParameter = shardingContexts.getJobParameter();
+ this.shardingItem = shardingItem;
+ shardingParameter = shardingContexts.getShardingItemParameters().get(shardingItem);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/dataflow/DataflowJob.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/dataflow/DataflowJob.java
new file mode 100644
index 0000000..008d0a1
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/dataflow/DataflowJob.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.api.dataflow;
+
+import io.elasticjob.cloud.api.ElasticJob;
+import io.elasticjob.cloud.api.ShardingContext;
+
+import java.util.List;
+
+/**
+ * 数据流分布式作业接口.
+ *
+ * @author zhangliang
+ *
+ * @param <T> 数据类型
+ */
+public interface DataflowJob<T> extends ElasticJob {
+
+ /**
+ * 获取待处理数据.
+ *
+ * @param shardingContext 分片上下文
+ * @return 待处理的数据集合
+ */
+ List<T> fetchData(ShardingContext shardingContext);
+
+ /**
+ * 处理数据.
+ *
+ * @param shardingContext 分片上下文
+ * @param data 待处理数据集合
+ */
+ void processData(ShardingContext shardingContext, List<T> data);
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/script/ScriptJob.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/script/ScriptJob.java
new file mode 100644
index 0000000..f221213
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/script/ScriptJob.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.api.script;
+
+import io.elasticjob.cloud.api.ElasticJob;
+
+/**
+ * 脚本分布式作业接口.
+ *
+ * @author zhangliang
+ */
+public interface ScriptJob extends ElasticJob {
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/simple/SimpleJob.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/simple/SimpleJob.java
new file mode 100644
index 0000000..d89d0b4
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/api/simple/SimpleJob.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.api.simple;
+
+import io.elasticjob.cloud.api.ElasticJob;
+import io.elasticjob.cloud.api.ShardingContext;
+
+/**
+ * 简单分布式作业接口.
+ *
+ * @author zhangliang
+ */
+public interface SimpleJob extends ElasticJob {
+
+ /**
+ * 执行作业.
+ *
+ * @param shardingContext 分片上下文
+ */
+ void execute(ShardingContext shardingContext);
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/JobCoreConfiguration.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/JobCoreConfiguration.java
new file mode 100644
index 0000000..d219cd7
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/JobCoreConfiguration.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.config;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import io.elasticjob.cloud.executor.handler.JobProperties;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 作业核心配置.
+ *
+ * @author zhangliang
+ */
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public final class JobCoreConfiguration {
+
+ private final String jobName;
+
+ private final String cron;
+
+ private final int shardingTotalCount;
+
+ private final String shardingItemParameters;
+
+ private final String jobParameter;
+
+ private final boolean failover;
+
+ private final boolean misfire;
+
+ private final String description;
+
+ private final JobProperties jobProperties;
+
+ /**
+ * 创建简单作业配置构建器.
+ *
+ * @param jobName 作业名称
+ * @param cron 作业启动时间的cron表达式
+ * @param shardingTotalCount 作业分片总数
+ * @return 简单作业配置构建器
+ */
+ public static Builder newBuilder(final String jobName, final String cron, final int shardingTotalCount) {
+ return new Builder(jobName, cron, shardingTotalCount);
+ }
+
+ @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+ public static class Builder {
+
+ private final String jobName;
+
+ private final String cron;
+
+ private final int shardingTotalCount;
+
+ private String shardingItemParameters = "";
+
+ private String jobParameter = "";
+
+ private boolean failover;
+
+ private boolean misfire = true;
+
+ private String description = "";
+
+ private final JobProperties jobProperties = new JobProperties();
+
+ /**
+ * 设置分片序列号和个性化参数对照表.
+ *
+ * <p>
+ * 分片序列号和参数用等号分隔, 多个键值对用逗号分隔. 类似map.
+ * 分片序列号从0开始, 不可大于或等于作业分片总数.
+ * 如:
+ * 0=a,1=b,2=c
+ * </p>
+ *
+ * @param shardingItemParameters 分片序列号和个性化参数对照表
+ *
+ * @return 作业配置构建器
+ */
+ public Builder shardingItemParameters(final String shardingItemParameters) {
+ if (null != shardingItemParameters) {
+ this.shardingItemParameters = shardingItemParameters;
+ }
+ return this;
+ }
+
+ /**
+ * 设置作业自定义参数.
+ *
+ * <p>
+ * 可以配置多个相同的作业, 但是用不同的参数作为不同的调度实例.
+ * </p>
+ *
+ * @param jobParameter 作业自定义参数
+ *
+ * @return 作业配置构建器
+ */
+ public Builder jobParameter(final String jobParameter) {
+ if (null != jobParameter) {
+ this.jobParameter = jobParameter;
+ }
+ return this;
+ }
+
+ /**
+ * 设置是否开启失效转移.
+ *
+ * <p>
+ * 只有对monitorExecution的情况下才可以开启失效转移.
+ * </p>
+ *
+ * @param failover 是否开启失效转移
+ *
+ * @return 作业配置构建器
+ */
+ public Builder failover(final boolean failover) {
+ this.failover = failover;
+ return this;
+ }
+
+ /**
+ * 设置是否开启misfire.
+ *
+ * @param misfire 是否开启misfire
+ *
+ * @return 作业配置构建器
+ */
+ public Builder misfire(final boolean misfire) {
+ this.misfire = misfire;
+ return this;
+ }
+
+ /**
+ * 设置作业描述信息.
+ *
+ * @param description 作业描述信息
+ *
+ * @return 作业配置构建器
+ */
+ public Builder description(final String description) {
+ if (null != description) {
+ this.description = description;
+ }
+ return this;
+ }
+
+ /**
+ * 设置作业属性.
+ *
+ * @param key 属性键
+ * @param value 属性值
+ *
+ * @return 作业配置构建器
+ */
+ public Builder jobProperties(final String key, final String value) {
+ jobProperties.put(key, value);
+ return this;
+ }
+
+ /**
+ * 构建作业配置对象.
+ *
+ * @return 作业配置对象
+ */
+ public final JobCoreConfiguration build() {
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(jobName), "jobName can not be empty.");
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(cron), "cron can not be empty.");
+ Preconditions.checkArgument(shardingTotalCount > 0, "shardingTotalCount should larger than zero.");
+ return new JobCoreConfiguration(jobName, cron, shardingTotalCount, shardingItemParameters, jobParameter, failover, misfire, description, jobProperties);
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/JobRootConfiguration.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/JobRootConfiguration.java
new file mode 100644
index 0000000..168895f
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/JobRootConfiguration.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.config;
+
+/**
+ * 作业配置根接口.
+ *
+ * @author zhangliang
+ */
+public interface JobRootConfiguration {
+
+ /**
+ * 获取作业类型配置.
+ *
+ * @return 作业类型配置
+ */
+ JobTypeConfiguration getTypeConfig();
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/JobTypeConfiguration.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/JobTypeConfiguration.java
new file mode 100644
index 0000000..41e8076
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/JobTypeConfiguration.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.config;
+
+import io.elasticjob.cloud.api.JobType;
+
+/**
+ * 作业类型配置.
+ *
+ * @author caohao
+ * @author zhangliang
+ */
+public interface JobTypeConfiguration {
+
+ /**
+ * 获取作业类型.
+ *
+ * @return 作业类型
+ */
+ JobType getJobType();
+
+ /**
+ * 获取作业实现类名称.
+ *
+ * @return 作业实现类名称
+ */
+ String getJobClass();
+
+ /**
+ * 获取作业核心配置.
+ *
+ * @return 作业核心配置
+ */
+ JobCoreConfiguration getCoreConfig();
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/dataflow/DataflowJobConfiguration.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/dataflow/DataflowJobConfiguration.java
new file mode 100644
index 0000000..46c06d6
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/dataflow/DataflowJobConfiguration.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.config.dataflow;
+
+import io.elasticjob.cloud.api.JobType;
+import io.elasticjob.cloud.config.JobCoreConfiguration;
+import io.elasticjob.cloud.config.JobTypeConfiguration;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 数据流作业配置信息.
+ *
+ * @author caohao
+ * @author zhangliang
+ */
+@RequiredArgsConstructor
+@Getter
+public final class DataflowJobConfiguration implements JobTypeConfiguration {
+
+ private final JobCoreConfiguration coreConfig;
+
+ private final JobType jobType = JobType.DATAFLOW;
+
+ private final String jobClass;
+
+ private final boolean streamingProcess;
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/script/ScriptJobConfiguration.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/script/ScriptJobConfiguration.java
new file mode 100644
index 0000000..545f187
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/script/ScriptJobConfiguration.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.config.script;
+
+import io.elasticjob.cloud.api.JobType;
+import io.elasticjob.cloud.api.script.ScriptJob;
+import io.elasticjob.cloud.config.JobTypeConfiguration;
+import io.elasticjob.cloud.config.JobCoreConfiguration;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 脚本作业配置.
+ *
+ * @author caohao
+ * @author zhangliang
+ */
+@RequiredArgsConstructor
+@Getter
+public final class ScriptJobConfiguration implements JobTypeConfiguration {
+
+ private final JobCoreConfiguration coreConfig;
+
+ private final JobType jobType = JobType.SCRIPT;
+
+ private final String jobClass = ScriptJob.class.getCanonicalName();
+
+ private final String scriptCommandLine;
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/simple/SimpleJobConfiguration.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/simple/SimpleJobConfiguration.java
new file mode 100644
index 0000000..ad63a1b
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/config/simple/SimpleJobConfiguration.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.config.simple;
+
+import io.elasticjob.cloud.api.JobType;
+import io.elasticjob.cloud.config.JobCoreConfiguration;
+import io.elasticjob.cloud.config.JobTypeConfiguration;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 简单作业配置.
+ *
+ * @author caohao
+ * @author zhangliang
+ */
+@RequiredArgsConstructor
+@Getter
+public final class SimpleJobConfiguration implements JobTypeConfiguration {
+
+ private final JobCoreConfiguration coreConfig;
+
+ private final JobType jobType = JobType.SIMPLE;
+
+ private final String jobClass;
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/context/ExecutionType.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/context/ExecutionType.java
new file mode 100644
index 0000000..b564c0b
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/context/ExecutionType.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.context;
+
+/**
+ * 执行类型.
+ *
+ * @author zhangliang
+ */
+public enum ExecutionType {
+
+ /**
+ * 准备执行的任务.
+ */
+ READY,
+
+ /**
+ * 失效转移的任务.
+ */
+ FAILOVER
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/context/TaskContext.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/context/TaskContext.java
new file mode 100644
index 0000000..3394de0
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/context/TaskContext.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.context;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * 任务运行时上下文.
+ *
+ * @author zhangliang
+ * @author caohao
+ */
+@Getter
+@EqualsAndHashCode(of = "id")
+@ToString(of = "id")
+public final class TaskContext {
+
+ private static final String DELIMITER = "@-@";
+
+ private static final String UNASSIGNED_SLAVE_ID = "unassigned-slave";
+
+ private String id;
+
+ private final MetaInfo metaInfo;
+
+ private final ExecutionType type;
+
+ private String slaveId;
+
+ @Setter
+ private boolean idle;
+
+ public TaskContext(final String jobName, final List<Integer> shardingItem, final ExecutionType type) {
+ this(jobName, shardingItem, type, UNASSIGNED_SLAVE_ID);
+ }
+
+ public TaskContext(final String jobName, final List<Integer> shardingItem, final ExecutionType type, final String slaveId) {
+ metaInfo = new MetaInfo(jobName, shardingItem);
+ this.type = type;
+ this.slaveId = slaveId;
+ id = Joiner.on(DELIMITER).join(metaInfo, type, slaveId, UUID.randomUUID().toString());
+ }
+
+ private TaskContext(final String id, final MetaInfo metaInfo, final ExecutionType type, final String slaveId) {
+ this.id = id;
+ this.metaInfo = metaInfo;
+ this.type = type;
+ this.slaveId = slaveId;
+ }
+
+ /**
+ * 根据任务主键获取任务上下文.
+ *
+ * @param id 任务主键
+ * @return 任务上下文
+ */
+ public static TaskContext from(final String id) {
+ String[] result = id.split(DELIMITER);
+ Preconditions.checkState(5 == result.length);
+ return new TaskContext(id, MetaInfo.from(result[0] + DELIMITER + result[1]), ExecutionType.valueOf(result[2]), result[3]);
+ }
+
+ /**
+ * 获取未分配执行服务器前的任务主键.
+ *
+ * @param id 任务主键
+ * @return 未分配执行服务器前的任务主键
+ */
+ public static String getIdForUnassignedSlave(final String id) {
+ return id.replaceAll(TaskContext.from(id).getSlaveId(), UNASSIGNED_SLAVE_ID);
+ }
+
+ /**
+ * 设置任务执行服务器主键.
+ *
+ * @param slaveId 任务执行服务器主键
+ */
+ public void setSlaveId(final String slaveId) {
+ id = id.replaceAll(this.slaveId, slaveId);
+ this.slaveId = slaveId;
+ }
+
+ /**
+ * 获取任务名称.
+ *
+ * @return 任务名称
+ */
+ public String getTaskName() {
+ return Joiner.on(DELIMITER).join(metaInfo, type, slaveId);
+ }
+
+ /**
+ * 获取任务执行器主键.
+ *
+ * @param appName 应用名称
+ * @return 任务执行器主键
+ */
+ public String getExecutorId(final String appName) {
+ return Joiner.on(DELIMITER).join(appName, slaveId);
+ }
+
+ /**
+ * 任务元信息.
+ */
+ @RequiredArgsConstructor
+ @Getter
+ @EqualsAndHashCode
+ public static class MetaInfo {
+
+ private final String jobName;
+
+ private final List<Integer> shardingItems;
+
+ /**
+ * 根据任务元信息字符串获取元信息对象.
+ *
+ * @param value 任务元信息字符串
+ * @return 元信息对象
+ */
+ public static MetaInfo from(final String value) {
+ String[] result = value.split(DELIMITER);
+ Preconditions.checkState(1 == result.length || 2 == result.length || 5 == result.length);
+ return new MetaInfo(result[0], 1 == result.length || "".equals(result[1]) ? Collections.<Integer>emptyList() : Lists.transform(Splitter.on(",").splitToList(result[1]),
+ new Function<String, Integer>() {
+
+ @Override
+ public Integer apply(final String input) {
+ return Integer.parseInt(input);
+ }
+ }));
+ }
+
+ @Override
+ public String toString() {
+ return Joiner.on(DELIMITER).join(jobName, Joiner.on(",").join(shardingItems));
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEvent.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEvent.java
new file mode 100644
index 0000000..df0de56
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEvent.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event;
+
+/**
+ * 作业事件接口.
+ *
+ * @author zhangliang
+ */
+public interface JobEvent {
+
+ /**
+ * 获取作业名称.
+ *
+ * @return 作业名称
+ */
+ String getJobName();
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventBus.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventBus.java
new file mode 100644
index 0000000..398989a
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventBus.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event;
+
+import com.google.common.eventbus.AsyncEventBus;
+import com.google.common.eventbus.EventBus;
+import io.elasticjob.cloud.util.concurrent.ExecutorServiceObject;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 运行痕迹事件总线.
+ *
+ * @author zhangliang
+ * @author caohao
+ */
+@Slf4j
+public final class JobEventBus {
+
+ private final JobEventConfiguration jobEventConfig;
+
+ private final ExecutorServiceObject executorServiceObject;
+
+ private final EventBus eventBus;
+
+ private boolean isRegistered;
+
+ public JobEventBus() {
+ jobEventConfig = null;
+ executorServiceObject = null;
+ eventBus = null;
+ }
+
+ public JobEventBus(final JobEventConfiguration jobEventConfig) {
+ this.jobEventConfig = jobEventConfig;
+ executorServiceObject = new ExecutorServiceObject("job-event", Runtime.getRuntime().availableProcessors() * 2);
+ eventBus = new AsyncEventBus(executorServiceObject.createExecutorService());
+ register();
+ }
+
+ private void register() {
+ try {
+ eventBus.register(jobEventConfig.createJobEventListener());
+ isRegistered = true;
+ } catch (final JobEventListenerConfigurationException ex) {
+ log.error("Elastic job: create JobEventListener failure, error is: ", ex);
+ }
+ }
+
+ /**
+ * 发布事件.
+ *
+ * @param event 作业事件
+ */
+ public void post(final JobEvent event) {
+ if (isRegistered && !executorServiceObject.isShutdown()) {
+ eventBus.post(event);
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventConfiguration.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventConfiguration.java
new file mode 100644
index 0000000..9f872c6
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventConfiguration.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event;
+
+/**
+ * 作业事件配置标识接口.
+ *
+ * @author caohao
+ */
+public interface JobEventConfiguration extends JobEventIdentity {
+
+ /**
+ * 创建作业事件监听器.
+ *
+ * @return 作业事件监听器.
+ * @throws JobEventListenerConfigurationException 作业事件监听器配置异常
+ */
+ JobEventListener createJobEventListener() throws JobEventListenerConfigurationException;
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventIdentity.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventIdentity.java
new file mode 100644
index 0000000..e897888
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventIdentity.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event;
+
+/**
+ * 作业事件标识.
+ *
+ * @author zhangliang
+ */
+public interface JobEventIdentity {
+
+ /**
+ * 获取作业事件标识.
+ *
+ * @return 作业事件标识
+ */
+ String getIdentity();
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventListener.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventListener.java
new file mode 100644
index 0000000..7853bf6
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventListener.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event;
+
+
+import com.google.common.eventbus.AllowConcurrentEvents;
+import com.google.common.eventbus.Subscribe;
+import io.elasticjob.cloud.event.type.JobExecutionEvent;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+
+/**
+ * 作业事件监听器.
+ *
+ * @author zhangliang
+ */
+public interface JobEventListener extends JobEventIdentity {
+
+ /**
+ * 作业执行事件监听执行.
+ *
+ * @param jobExecutionEvent 作业执行事件
+ */
+ @Subscribe
+ @AllowConcurrentEvents
+ void listen(JobExecutionEvent jobExecutionEvent);
+
+ /**
+ * 作业状态痕迹事件监听执行.
+ *
+ * @param jobStatusTraceEvent 作业状态痕迹事件
+ */
+ @Subscribe
+ @AllowConcurrentEvents
+ void listen(JobStatusTraceEvent jobStatusTraceEvent);
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventListenerConfigurationException.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventListenerConfigurationException.java
new file mode 100644
index 0000000..2aaca24
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/JobEventListenerConfigurationException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event;
+
+/**
+ * 作业事件监听器配置异常.
+ *
+ * @author zhangliang
+ */
+public final class JobEventListenerConfigurationException extends Exception {
+
+ private static final long serialVersionUID = 4069519372148227761L;
+
+ public JobEventListenerConfigurationException(final Exception ex) {
+ super(ex);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/DatabaseType.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/DatabaseType.java
new file mode 100644
index 0000000..aecffab
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/DatabaseType.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.rdb;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterators;
+
+import java.util.Arrays;
+
+/**
+ * 支持的数据库类型.
+ *
+ * @author caohao
+ */
+public enum DatabaseType {
+
+ H2("H2"), MySQL("MySQL"), Oracle("Oracle"), SQLServer("Microsoft SQL Server"), DB2("DB2"), PostgreSQL("PostgreSQL");
+
+ private final String productName;
+
+ DatabaseType(final String productName) {
+ this.productName = productName;
+ }
+
+ /**
+ * 获取数据库类型枚举.
+ *
+ * @param databaseProductName 数据库类型
+ * @return 数据库类型枚举
+ */
+ public static DatabaseType valueFrom(final String databaseProductName) {
+ Optional<DatabaseType> databaseTypeOptional = Iterators.tryFind(Arrays.asList(DatabaseType.values()).iterator(), new Predicate<DatabaseType>() {
+ @Override
+ public boolean apply(final DatabaseType input) {
+ return input.productName.equals(databaseProductName);
+ }
+ });
+ if (databaseTypeOptional.isPresent()) {
+ return databaseTypeOptional.get();
+ } else {
+ throw new RuntimeException("Unsupported database:" + databaseProductName);
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbConfiguration.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbConfiguration.java
new file mode 100644
index 0000000..60502d7
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbConfiguration.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.rdb;
+
+import io.elasticjob.cloud.event.JobEventConfiguration;
+import io.elasticjob.cloud.event.JobEventListener;
+import io.elasticjob.cloud.event.JobEventListenerConfigurationException;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import javax.sql.DataSource;
+import java.io.Serializable;
+import java.sql.SQLException;
+
+/**
+ * 作业数据库事件配置.
+ *
+ * @author caohao
+ */
+@RequiredArgsConstructor
+@Getter
+public final class JobEventRdbConfiguration extends JobEventRdbIdentity implements JobEventConfiguration, Serializable {
+
+ private static final long serialVersionUID = 3344410699286435226L;
+
+ private final transient DataSource dataSource;
+
+ @Override
+ public JobEventListener createJobEventListener() throws JobEventListenerConfigurationException {
+ try {
+ return new JobEventRdbListener(dataSource);
+ } catch (final SQLException ex) {
+ throw new JobEventListenerConfigurationException(ex);
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbIdentity.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbIdentity.java
new file mode 100644
index 0000000..02f3c7b
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbIdentity.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.rdb;
+
+import io.elasticjob.cloud.event.JobEventIdentity;
+
+/**
+ * 关系型数据库作业事件标识.
+ *
+ * @author zhangliang
+ */
+public class JobEventRdbIdentity implements JobEventIdentity {
+
+ @Override
+ public String getIdentity() {
+ return "rdb";
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbListener.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbListener.java
new file mode 100644
index 0000000..32d14e7
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbListener.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.rdb;
+
+import io.elasticjob.cloud.event.JobEventListener;
+import io.elasticjob.cloud.event.type.JobExecutionEvent;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+
+import javax.sql.DataSource;
+import java.sql.SQLException;
+
+/**
+ * 运行痕迹事件数据库监听器.
+ *
+ * @author caohao
+ */
+public final class JobEventRdbListener extends JobEventRdbIdentity implements JobEventListener {
+
+ private final JobEventRdbStorage repository;
+
+ public JobEventRdbListener(final DataSource dataSource) throws SQLException {
+ repository = new JobEventRdbStorage(dataSource);
+ }
+
+ @Override
+ public void listen(final JobExecutionEvent executionEvent) {
+ repository.addJobExecutionEvent(executionEvent);
+ }
+
+ @Override
+ public void listen(final JobStatusTraceEvent jobStatusTraceEvent) {
+ repository.addJobStatusTraceEvent(jobStatusTraceEvent);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbSearch.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbSearch.java
new file mode 100644
index 0000000..d7f1637
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbSearch.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.rdb;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import io.elasticjob.cloud.context.ExecutionType;
+import io.elasticjob.cloud.event.type.JobExecutionEvent;
+import io.elasticjob.cloud.event.type.JobExecutionEventThrowable;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent.Source;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent.State;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 运行痕迹事件数据库检索.
+ *
+ * @author liguangyun
+ */
+@RequiredArgsConstructor
+@Slf4j
+public final class JobEventRdbSearch {
+
+ private static final String TABLE_JOB_EXECUTION_LOG = "JOB_EXECUTION_LOG";
+
+ private static final String TABLE_JOB_STATUS_TRACE_LOG = "JOB_STATUS_TRACE_LOG";
+
+ private static final List<String> FIELDS_JOB_EXECUTION_LOG =
+ Lists.newArrayList("id", "hostname", "ip", "task_id", "job_name", "execution_source", "sharding_item", "start_time", "complete_time", "is_success", "failure_cause");
+
+ private static final List<String> FIELDS_JOB_STATUS_TRACE_LOG =
+ Lists.newArrayList("id", "job_name", "original_task_id", "task_id", "slave_id", "source", "execution_type", "sharding_item", "state", "message", "creation_time");
+
+ private final DataSource dataSource;
+
+ /**
+ * 检索作业运行执行轨迹.
+ *
+ * @param condition 查询条件
+ * @return 作业执行轨迹检索结果
+ */
+ public Result<JobExecutionEvent> findJobExecutionEvents(final Condition condition) {
+ return new Result<>(getEventCount(TABLE_JOB_EXECUTION_LOG, FIELDS_JOB_EXECUTION_LOG, condition), getJobExecutionEvents(condition));
+ }
+
+ /**
+ * 检索作业运行状态轨迹.
+ *
+ * @param condition 查询条件
+ * @return 作业状态轨迹检索结果
+ */
+ public Result<JobStatusTraceEvent> findJobStatusTraceEvents(final Condition condition) {
+ return new Result<>(getEventCount(TABLE_JOB_STATUS_TRACE_LOG, FIELDS_JOB_STATUS_TRACE_LOG, condition), getJobStatusTraceEvents(condition));
+ }
+
+ private List<JobExecutionEvent> getJobExecutionEvents(final Condition condition) {
+ List<JobExecutionEvent> result = new LinkedList<>();
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = createDataPreparedStatement(conn, TABLE_JOB_EXECUTION_LOG, FIELDS_JOB_EXECUTION_LOG, condition);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ while (resultSet.next()) {
+ JobExecutionEvent jobExecutionEvent = new JobExecutionEvent(resultSet.getString(1), resultSet.getString(2), resultSet.getString(3), resultSet.getString(4),
+ resultSet.getString(5), JobExecutionEvent.ExecutionSource.valueOf(resultSet.getString(6)), Integer.valueOf(resultSet.getString(7)),
+ new Date(resultSet.getTimestamp(8).getTime()), resultSet.getTimestamp(9) == null ? null : new Date(resultSet.getTimestamp(9).getTime()),
+ resultSet.getBoolean(10), new JobExecutionEventThrowable(null, resultSet.getString(11))
+ );
+ result.add(jobExecutionEvent);
+ }
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Fetch JobExecutionEvent from DB error:", ex);
+ }
+ return result;
+ }
+
+ private List<JobStatusTraceEvent> getJobStatusTraceEvents(final Condition condition) {
+ List<JobStatusTraceEvent> result = new LinkedList<>();
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = createDataPreparedStatement(conn, TABLE_JOB_STATUS_TRACE_LOG, FIELDS_JOB_STATUS_TRACE_LOG, condition);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ while (resultSet.next()) {
+ JobStatusTraceEvent jobStatusTraceEvent = new JobStatusTraceEvent(resultSet.getString(1), resultSet.getString(2), resultSet.getString(3), resultSet.getString(4),
+ resultSet.getString(5), Source.valueOf(resultSet.getString(6)), ExecutionType.valueOf(resultSet.getString(7)), resultSet.getString(8),
+ State.valueOf(resultSet.getString(9)), resultSet.getString(10), new Date(resultSet.getTimestamp(11).getTime()));
+ result.add(jobStatusTraceEvent);
+ }
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Fetch JobStatusTraceEvent from DB error:", ex);
+ }
+ return result;
+ }
+
+ private int getEventCount(final String tableName, final Collection<String> tableFields, final Condition condition) {
+ int result = 0;
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = createCountPreparedStatement(conn, tableName, tableFields, condition);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ resultSet.next();
+ result = resultSet.getInt(1);
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Fetch EventCount from DB error:", ex);
+ }
+ return result;
+ }
+
+ private PreparedStatement createDataPreparedStatement(final Connection conn, final String tableName, final Collection<String> tableFields, final Condition condition) throws SQLException {
+ String sql = buildDataSql(tableName, tableFields, condition);
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ setBindValue(preparedStatement, tableFields, condition);
+ return preparedStatement;
+ }
+
+ private PreparedStatement createCountPreparedStatement(final Connection conn, final String tableName, final Collection<String> tableFields, final Condition condition) throws SQLException {
+ String sql = buildCountSql(tableName, tableFields, condition);
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ setBindValue(preparedStatement, tableFields, condition);
+ return preparedStatement;
+ }
+
+ private String buildDataSql(final String tableName, final Collection<String> tableFields, final Condition condition) {
+ StringBuilder sqlBuilder = new StringBuilder();
+ String selectSql = buildSelect(tableName, tableFields);
+ String whereSql = buildWhere(tableName, tableFields, condition);
+ String orderSql = buildOrder(tableFields, condition.getSort(), condition.getOrder());
+ String limitSql = buildLimit(condition.getPage(), condition.getPerPage());
+ sqlBuilder.append(selectSql).append(whereSql).append(orderSql).append(limitSql);
+ return sqlBuilder.toString();
+ }
+
+ private String buildCountSql(final String tableName, final Collection<String> tableFields, final Condition condition) {
+ StringBuilder sqlBuilder = new StringBuilder();
+ String selectSql = buildSelectCount(tableName);
+ String whereSql = buildWhere(tableName, tableFields, condition);
+ sqlBuilder.append(selectSql).append(whereSql);
+ return sqlBuilder.toString();
+ }
+
+ private String buildSelectCount(final String tableName) {
+ return String.format("SELECT COUNT(1) FROM %s", tableName);
+ }
+
+ private String buildSelect(final String tableName, final Collection<String> tableFields) {
+ StringBuilder sqlBuilder = new StringBuilder();
+ sqlBuilder.append("SELECT ");
+ for (String each : tableFields) {
+ sqlBuilder.append(each).append(",");
+ }
+ sqlBuilder.deleteCharAt(sqlBuilder.length() - 1);
+ sqlBuilder.append(" FROM ").append(tableName);
+ return sqlBuilder.toString();
+ }
+
+ private String buildWhere(final String tableName, final Collection<String> tableFields, final Condition condition) {
+ StringBuilder sqlBuilder = new StringBuilder();
+ sqlBuilder.append(" WHERE 1=1");
+ if (null != condition.getFields() && !condition.getFields().isEmpty()) {
+ for (Map.Entry<String, Object> entry : condition.getFields().entrySet()) {
+ String lowerUnderscore = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, entry.getKey());
+ if (null != entry.getValue() && tableFields.contains(lowerUnderscore)) {
+ sqlBuilder.append(" AND ").append(lowerUnderscore).append("=?");
+ }
+ }
+ }
+ if (null != condition.getStartTime()) {
+ sqlBuilder.append(" AND ").append(getTableTimeField(tableName)).append(">=?");
+ }
+ if (null != condition.getEndTime()) {
+ sqlBuilder.append(" AND ").append(getTableTimeField(tableName)).append("<=?");
+ }
+ return sqlBuilder.toString();
+ }
+
+ private void setBindValue(final PreparedStatement preparedStatement, final Collection<String> tableFields, final Condition condition) throws SQLException {
+ int index = 1;
+ if (null != condition.getFields() && !condition.getFields().isEmpty()) {
+ for (Map.Entry<String, Object> entry : condition.getFields().entrySet()) {
+ String lowerUnderscore = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, entry.getKey());
+ if (null != entry.getValue() && tableFields.contains(lowerUnderscore)) {
+ preparedStatement.setString(index++, String.valueOf(entry.getValue()));
+ }
+ }
+ }
+ if (null != condition.getStartTime()) {
+ preparedStatement.setTimestamp(index++, new Timestamp(condition.getStartTime().getTime()));
+ }
+ if (null != condition.getEndTime()) {
+ preparedStatement.setTimestamp(index, new Timestamp(condition.getEndTime().getTime()));
+ }
+ }
+
+ private String getTableTimeField(final String tableName) {
+ String result = "";
+ if (TABLE_JOB_EXECUTION_LOG.equals(tableName)) {
+ result = "start_time";
+ } else if (TABLE_JOB_STATUS_TRACE_LOG.equals(tableName)) {
+ result = "creation_time";
+ }
+ return result;
+ }
+
+ private String buildOrder(final Collection<String> tableFields, final String sortName, final String sortOrder) {
+ if (Strings.isNullOrEmpty(sortName)) {
+ return "";
+ }
+ String lowerUnderscore = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, sortName);
+ if (!tableFields.contains(lowerUnderscore)) {
+ return "";
+ }
+ StringBuilder sqlBuilder = new StringBuilder();
+ sqlBuilder.append(" ORDER BY ").append(lowerUnderscore);
+ switch (sortOrder.toUpperCase()) {
+ case "ASC":
+ sqlBuilder.append(" ASC");
+ break;
+ case "DESC":
+ sqlBuilder.append(" DESC");
+ break;
+ default :
+ sqlBuilder.append(" ASC");
+ }
+ return sqlBuilder.toString();
+ }
+
+ private String buildLimit(final int page, final int perPage) {
+ StringBuilder sqlBuilder = new StringBuilder();
+ if (page > 0 && perPage > 0) {
+ sqlBuilder.append(" LIMIT ").append((page - 1) * perPage).append(",").append(perPage);
+ } else {
+ sqlBuilder.append(" LIMIT ").append(Condition.DEFAULT_PAGE_SIZE);
+ }
+ return sqlBuilder.toString();
+ }
+
+ /**
+ * 查询条件对象.
+ *
+ * @author liguangyun
+ */
+ @RequiredArgsConstructor
+ @Getter
+ public static class Condition {
+
+ private static final int DEFAULT_PAGE_SIZE = 10;
+
+ private final int perPage;
+
+ private final int page;
+
+ private final String sort;
+
+ private final String order;
+
+ private final Date startTime;
+
+ private final Date endTime;
+
+ private final Map<String, Object> fields;
+ }
+
+ @RequiredArgsConstructor
+ @Getter
+ public static class Result<T> {
+
+ private final Integer total;
+
+ private final List<T> rows;
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbStorage.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbStorage.java
new file mode 100644
index 0000000..c9f1d9a
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/rdb/JobEventRdbStorage.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.rdb;
+
+import com.google.common.base.Strings;
+import io.elasticjob.cloud.context.ExecutionType;
+import io.elasticjob.cloud.event.type.JobExecutionEvent;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * 运行痕迹事件数据库存储.
+ *
+ * @author caohao
+ */
+@Slf4j
+final class JobEventRdbStorage {
+
+ private static final String TABLE_JOB_EXECUTION_LOG = "JOB_EXECUTION_LOG";
+
+ private static final String TABLE_JOB_STATUS_TRACE_LOG = "JOB_STATUS_TRACE_LOG";
+
+ private static final String TASK_ID_STATE_INDEX = "TASK_ID_STATE_INDEX";
+
+ private final DataSource dataSource;
+
+ private DatabaseType databaseType;
+
+ JobEventRdbStorage(final DataSource dataSource) throws SQLException {
+ this.dataSource = dataSource;
+ initTablesAndIndexes();
+ }
+
+ private void initTablesAndIndexes() throws SQLException {
+ try (Connection conn = dataSource.getConnection()) {
+ createJobExecutionTableAndIndexIfNeeded(conn);
+ createJobStatusTraceTableAndIndexIfNeeded(conn);
+ databaseType = DatabaseType.valueFrom(conn.getMetaData().getDatabaseProductName());
+ }
+ }
+
+ private void createJobExecutionTableAndIndexIfNeeded(final Connection conn) throws SQLException {
+ DatabaseMetaData dbMetaData = conn.getMetaData();
+ try (ResultSet resultSet = dbMetaData.getTables(null, null, TABLE_JOB_EXECUTION_LOG, new String[]{"TABLE"})) {
+ if (!resultSet.next()) {
+ createJobExecutionTable(conn);
+ }
+ }
+ }
+
+ private void createJobStatusTraceTableAndIndexIfNeeded(final Connection conn) throws SQLException {
+ DatabaseMetaData dbMetaData = conn.getMetaData();
+ try (ResultSet resultSet = dbMetaData.getTables(null, null, TABLE_JOB_STATUS_TRACE_LOG, new String[]{"TABLE"})) {
+ if (!resultSet.next()) {
+ createJobStatusTraceTable(conn);
+ }
+ }
+ createTaskIdIndexIfNeeded(conn, TABLE_JOB_STATUS_TRACE_LOG, TASK_ID_STATE_INDEX);
+ }
+
+ private void createTaskIdIndexIfNeeded(final Connection conn, final String tableName, final String indexName) throws SQLException {
+ DatabaseMetaData dbMetaData = conn.getMetaData();
+ try (ResultSet resultSet = dbMetaData.getIndexInfo(null, null, tableName, false, false)) {
+ boolean hasTaskIdIndex = false;
+ while (resultSet.next()) {
+ if (indexName.equals(resultSet.getString("INDEX_NAME"))) {
+ hasTaskIdIndex = true;
+ }
+ }
+ if (!hasTaskIdIndex) {
+ createTaskIdAndStateIndex(conn, tableName);
+ }
+ }
+ }
+
+ private void createJobExecutionTable(final Connection conn) throws SQLException {
+ String dbSchema = "CREATE TABLE `" + TABLE_JOB_EXECUTION_LOG + "` ("
+ + "`id` VARCHAR(40) NOT NULL, "
+ + "`job_name` VARCHAR(100) NOT NULL, "
+ + "`task_id` VARCHAR(255) NOT NULL, "
+ + "`hostname` VARCHAR(255) NOT NULL, "
+ + "`ip` VARCHAR(50) NOT NULL, "
+ + "`sharding_item` INT NOT NULL, "
+ + "`execution_source` VARCHAR(20) NOT NULL, "
+ + "`failure_cause` VARCHAR(4000) NULL, "
+ + "`is_success` INT NOT NULL, "
+ + "`start_time` TIMESTAMP NULL, "
+ + "`complete_time` TIMESTAMP NULL, "
+ + "PRIMARY KEY (`id`));";
+ try (PreparedStatement preparedStatement = conn.prepareStatement(dbSchema)) {
+ preparedStatement.execute();
+ }
+ }
+
+ private void createJobStatusTraceTable(final Connection conn) throws SQLException {
+ String dbSchema = "CREATE TABLE `" + TABLE_JOB_STATUS_TRACE_LOG + "` ("
+ + "`id` VARCHAR(40) NOT NULL, "
+ + "`job_name` VARCHAR(100) NOT NULL, "
+ + "`original_task_id` VARCHAR(255) NOT NULL, "
+ + "`task_id` VARCHAR(255) NOT NULL, "
+ + "`slave_id` VARCHAR(50) NOT NULL, "
+ + "`source` VARCHAR(50) NOT NULL, "
+ + "`execution_type` VARCHAR(20) NOT NULL, "
+ + "`sharding_item` VARCHAR(100) NOT NULL, "
+ + "`state` VARCHAR(20) NOT NULL, "
+ + "`message` VARCHAR(4000) NULL, "
+ + "`creation_time` TIMESTAMP NULL, "
+ + "PRIMARY KEY (`id`));";
+ try (PreparedStatement preparedStatement = conn.prepareStatement(dbSchema)) {
+ preparedStatement.execute();
+ }
+ }
+
+ private void createTaskIdAndStateIndex(final Connection conn, final String tableName) throws SQLException {
+ String sql = "CREATE INDEX " + TASK_ID_STATE_INDEX + " ON " + tableName + " (`task_id`, `state`);";
+ try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
+ preparedStatement.execute();
+ }
+ }
+
+ boolean addJobExecutionEvent(final JobExecutionEvent jobExecutionEvent) {
+ if (null == jobExecutionEvent.getCompleteTime()) {
+ return insertJobExecutionEvent(jobExecutionEvent);
+ } else {
+ if (jobExecutionEvent.isSuccess()) {
+ return updateJobExecutionEventWhenSuccess(jobExecutionEvent);
+ } else {
+ return updateJobExecutionEventFailure(jobExecutionEvent);
+ }
+ }
+ }
+
+ private boolean insertJobExecutionEvent(final JobExecutionEvent jobExecutionEvent) {
+ boolean result = false;
+ String sql = "INSERT INTO `" + TABLE_JOB_EXECUTION_LOG + "` (`id`, `job_name`, `task_id`, `hostname`, `ip`, `sharding_item`, `execution_source`, `is_success`, `start_time`) "
+ + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);";
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
+ preparedStatement.setString(1, jobExecutionEvent.getId());
+ preparedStatement.setString(2, jobExecutionEvent.getJobName());
+ preparedStatement.setString(3, jobExecutionEvent.getTaskId());
+ preparedStatement.setString(4, jobExecutionEvent.getHostname());
+ preparedStatement.setString(5, jobExecutionEvent.getIp());
+ preparedStatement.setInt(6, jobExecutionEvent.getShardingItem());
+ preparedStatement.setString(7, jobExecutionEvent.getSource().toString());
+ preparedStatement.setBoolean(8, jobExecutionEvent.isSuccess());
+ preparedStatement.setTimestamp(9, new Timestamp(jobExecutionEvent.getStartTime().getTime()));
+ preparedStatement.execute();
+ result = true;
+ } catch (final SQLException ex) {
+ if (!isDuplicateRecord(ex)) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error(ex.getMessage());
+ }
+ }
+ return result;
+ }
+
+ private boolean isDuplicateRecord(final SQLException ex) {
+ return DatabaseType.MySQL.equals(databaseType) && 1062 == ex.getErrorCode() || DatabaseType.H2.equals(databaseType) && 23505 == ex.getErrorCode()
+ || DatabaseType.SQLServer.equals(databaseType) && 1 == ex.getErrorCode() || DatabaseType.DB2.equals(databaseType) && -803 == ex.getErrorCode()
+ || DatabaseType.PostgreSQL.equals(databaseType) && 0 == ex.getErrorCode() || DatabaseType.Oracle.equals(databaseType) && 1 == ex.getErrorCode();
+ }
+
+ private boolean updateJobExecutionEventWhenSuccess(final JobExecutionEvent jobExecutionEvent) {
+ boolean result = false;
+ String sql = "UPDATE `" + TABLE_JOB_EXECUTION_LOG + "` SET `is_success` = ?, `complete_time` = ? WHERE id = ?";
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
+ preparedStatement.setBoolean(1, jobExecutionEvent.isSuccess());
+ preparedStatement.setTimestamp(2, new Timestamp(jobExecutionEvent.getCompleteTime().getTime()));
+ preparedStatement.setString(3, jobExecutionEvent.getId());
+ if (0 == preparedStatement.executeUpdate()) {
+ return insertJobExecutionEventWhenSuccess(jobExecutionEvent);
+ }
+ result = true;
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error(ex.getMessage());
+ }
+ return result;
+ }
+
+ private boolean insertJobExecutionEventWhenSuccess(final JobExecutionEvent jobExecutionEvent) {
+ boolean result = false;
+ String sql = "INSERT INTO `" + TABLE_JOB_EXECUTION_LOG + "` (`id`, `job_name`, `task_id`, `hostname`, `ip`, `sharding_item`, `execution_source`, `is_success`, `start_time`, `complete_time`) "
+ + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
+ preparedStatement.setString(1, jobExecutionEvent.getId());
+ preparedStatement.setString(2, jobExecutionEvent.getJobName());
+ preparedStatement.setString(3, jobExecutionEvent.getTaskId());
+ preparedStatement.setString(4, jobExecutionEvent.getHostname());
+ preparedStatement.setString(5, jobExecutionEvent.getIp());
+ preparedStatement.setInt(6, jobExecutionEvent.getShardingItem());
+ preparedStatement.setString(7, jobExecutionEvent.getSource().toString());
+ preparedStatement.setBoolean(8, jobExecutionEvent.isSuccess());
+ preparedStatement.setTimestamp(9, new Timestamp(jobExecutionEvent.getStartTime().getTime()));
+ preparedStatement.setTimestamp(10, new Timestamp(jobExecutionEvent.getCompleteTime().getTime()));
+ preparedStatement.execute();
+ result = true;
+ } catch (final SQLException ex) {
+ if (isDuplicateRecord(ex)) {
+ return updateJobExecutionEventWhenSuccess(jobExecutionEvent);
+ }
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error(ex.getMessage());
+ }
+ return result;
+ }
+
+ private boolean updateJobExecutionEventFailure(final JobExecutionEvent jobExecutionEvent) {
+ boolean result = false;
+ String sql = "UPDATE `" + TABLE_JOB_EXECUTION_LOG + "` SET `is_success` = ?, `complete_time` = ?, `failure_cause` = ? WHERE id = ?";
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
+ preparedStatement.setBoolean(1, jobExecutionEvent.isSuccess());
+ preparedStatement.setTimestamp(2, new Timestamp(jobExecutionEvent.getCompleteTime().getTime()));
+ preparedStatement.setString(3, truncateString(jobExecutionEvent.getFailureCause()));
+ preparedStatement.setString(4, jobExecutionEvent.getId());
+ if (0 == preparedStatement.executeUpdate()) {
+ return insertJobExecutionEventWhenFailure(jobExecutionEvent);
+ }
+ result = true;
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error(ex.getMessage());
+ }
+ return result;
+ }
+
+ private boolean insertJobExecutionEventWhenFailure(final JobExecutionEvent jobExecutionEvent) {
+ boolean result = false;
+ String sql = "INSERT INTO `" + TABLE_JOB_EXECUTION_LOG + "` (`id`, `job_name`, `task_id`, `hostname`, `ip`, `sharding_item`, `execution_source`, `failure_cause`, `is_success`, `start_time`) "
+ + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
+ preparedStatement.setString(1, jobExecutionEvent.getId());
+ preparedStatement.setString(2, jobExecutionEvent.getJobName());
+ preparedStatement.setString(3, jobExecutionEvent.getTaskId());
+ preparedStatement.setString(4, jobExecutionEvent.getHostname());
+ preparedStatement.setString(5, jobExecutionEvent.getIp());
+ preparedStatement.setInt(6, jobExecutionEvent.getShardingItem());
+ preparedStatement.setString(7, jobExecutionEvent.getSource().toString());
+ preparedStatement.setString(8, truncateString(jobExecutionEvent.getFailureCause()));
+ preparedStatement.setBoolean(9, jobExecutionEvent.isSuccess());
+ preparedStatement.setTimestamp(10, new Timestamp(jobExecutionEvent.getStartTime().getTime()));
+ preparedStatement.execute();
+ result = true;
+ } catch (final SQLException ex) {
+ if (isDuplicateRecord(ex)) {
+ return updateJobExecutionEventFailure(jobExecutionEvent);
+ }
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error(ex.getMessage());
+ }
+ return result;
+ }
+
+ boolean addJobStatusTraceEvent(final JobStatusTraceEvent jobStatusTraceEvent) {
+ String originalTaskId = jobStatusTraceEvent.getOriginalTaskId();
+ if (JobStatusTraceEvent.State.TASK_STAGING != jobStatusTraceEvent.getState()) {
+ originalTaskId = getOriginalTaskId(jobStatusTraceEvent.getTaskId());
+ }
+ boolean result = false;
+ String sql = "INSERT INTO `" + TABLE_JOB_STATUS_TRACE_LOG + "` (`id`, `job_name`, `original_task_id`, `task_id`, `slave_id`, `source`, `execution_type`, `sharding_item`, "
+ + "`state`, `message`, `creation_time`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
+ preparedStatement.setString(1, UUID.randomUUID().toString());
+ preparedStatement.setString(2, jobStatusTraceEvent.getJobName());
+ preparedStatement.setString(3, originalTaskId);
+ preparedStatement.setString(4, jobStatusTraceEvent.getTaskId());
+ preparedStatement.setString(5, jobStatusTraceEvent.getSlaveId());
+ preparedStatement.setString(6, jobStatusTraceEvent.getSource().toString());
+ preparedStatement.setString(7, jobStatusTraceEvent.getExecutionType().name());
+ preparedStatement.setString(8, jobStatusTraceEvent.getShardingItems());
+ preparedStatement.setString(9, jobStatusTraceEvent.getState().toString());
+ preparedStatement.setString(10, truncateString(jobStatusTraceEvent.getMessage()));
+ preparedStatement.setTimestamp(11, new Timestamp(jobStatusTraceEvent.getCreationTime().getTime()));
+ preparedStatement.execute();
+ result = true;
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error(ex.getMessage());
+ }
+ return result;
+ }
+
+ private String getOriginalTaskId(final String taskId) {
+ String sql = String.format("SELECT original_task_id FROM %s WHERE task_id = '%s' and state='%s'", TABLE_JOB_STATUS_TRACE_LOG, taskId, JobStatusTraceEvent.State.TASK_STAGING);
+ String result = "";
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ if (resultSet.next()) {
+ return resultSet.getString("original_task_id");
+ }
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error(ex.getMessage());
+ }
+ return result;
+ }
+
+ private String truncateString(final String str) {
+ return !Strings.isNullOrEmpty(str) && str.length() > 4000 ? str.substring(0, 4000) : str;
+ }
+
+ List<JobStatusTraceEvent> getJobStatusTraceEvents(final String taskId) {
+ String sql = String.format("SELECT * FROM %s WHERE task_id = '%s'", TABLE_JOB_STATUS_TRACE_LOG, taskId);
+ List<JobStatusTraceEvent> result = new ArrayList<>();
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ while (resultSet.next()) {
+ JobStatusTraceEvent jobStatusTraceEvent = new JobStatusTraceEvent(resultSet.getString(1), resultSet.getString(2), resultSet.getString(3), resultSet.getString(4),
+ resultSet.getString(5), JobStatusTraceEvent.Source.valueOf(resultSet.getString(6)), ExecutionType.valueOf(resultSet.getString(7)), resultSet.getString(8),
+ JobStatusTraceEvent.State.valueOf(resultSet.getString(9)), resultSet.getString(10), new SimpleDateFormat("yyyy-mm-dd HH:MM:SS").parse(resultSet.getString(11)));
+ result.add(jobStatusTraceEvent);
+ }
+ } catch (final SQLException | ParseException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error(ex.getMessage());
+ }
+ return result;
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/type/JobExecutionEvent.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/type/JobExecutionEvent.java
new file mode 100644
index 0000000..5e34dac
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/type/JobExecutionEvent.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.type;
+
+import io.elasticjob.cloud.exception.ExceptionUtil;
+import io.elasticjob.cloud.event.JobEvent;
+import io.elasticjob.cloud.util.env.IpUtils;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ * 作业执行事件.
+ *
+ * @author zhangliang
+ */
+@RequiredArgsConstructor
+@AllArgsConstructor
+@Getter
+public final class JobExecutionEvent implements JobEvent {
+
+ private String id = UUID.randomUUID().toString();
+
+ private String hostname = IpUtils.getHostName();
+
+ private String ip = IpUtils.getIp();
+
+ private final String taskId;
+
+ private final String jobName;
+
+ private final ExecutionSource source;
+
+ private final int shardingItem;
+
+ private Date startTime = new Date();
+
+ @Setter
+ private Date completeTime;
+
+ @Setter
+ private boolean success;
+
+ @Setter
+ private JobExecutionEventThrowable failureCause;
+
+ /**
+ * 作业执行成功.
+ *
+ * @return 作业执行事件
+ */
+ public JobExecutionEvent executionSuccess() {
+ JobExecutionEvent result = new JobExecutionEvent(id, hostname, ip, taskId, jobName, source, shardingItem, startTime, completeTime, success, failureCause);
+ result.setCompleteTime(new Date());
+ result.setSuccess(true);
+ return result;
+ }
+
+ /**
+ * 作业执行失败.
+ *
+ * @param failureCause 失败原因
+ * @return 作业执行事件
+ */
+ public JobExecutionEvent executionFailure(final Throwable failureCause) {
+ JobExecutionEvent result = new JobExecutionEvent(id, hostname, ip, taskId, jobName, source, shardingItem, startTime, completeTime, success, new JobExecutionEventThrowable(failureCause));
+ result.setCompleteTime(new Date());
+ result.setSuccess(false);
+ return result;
+ }
+
+ /**
+ * 获取失败原因.
+ *
+ * @return 失败原因
+ */
+ public String getFailureCause() {
+ return ExceptionUtil.transform(failureCause == null ? null : failureCause.getThrowable());
+ }
+
+ /**
+ * 执行来源.
+ */
+ public enum ExecutionSource {
+ NORMAL_TRIGGER, MISFIRE, FAILOVER
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/type/JobExecutionEventThrowable.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/type/JobExecutionEventThrowable.java
new file mode 100644
index 0000000..c05c2a0
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/type/JobExecutionEventThrowable.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.type;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+
+/**
+ * 作业执行事件Throwable.
+ *
+ * @author liguangyun
+ */
+@RequiredArgsConstructor
+@AllArgsConstructor
+@Getter
+@ToString(of = "plainText")
+public final class JobExecutionEventThrowable {
+
+ private final Throwable throwable;
+
+ private String plainText;
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/type/JobStatusTraceEvent.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/type/JobStatusTraceEvent.java
new file mode 100644
index 0000000..4a80937
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/event/type/JobStatusTraceEvent.java
@@ -0,0 +1,53 @@
+package io.elasticjob.cloud.event.type;
+
+import io.elasticjob.cloud.context.ExecutionType;
+import io.elasticjob.cloud.event.JobEvent;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ * 作业状态痕迹事件.
+ *
+ * @author caohao
+ */
+@RequiredArgsConstructor
+@AllArgsConstructor
+@Getter
+public final class JobStatusTraceEvent implements JobEvent {
+
+ private String id = UUID.randomUUID().toString();
+
+ private final String jobName;
+
+ @Setter
+ private String originalTaskId = "";
+
+ private final String taskId;
+
+ private final String slaveId;
+
+ private final Source source;
+
+ private final ExecutionType executionType;
+
+ private final String shardingItems;
+
+ private final State state;
+
+ private final String message;
+
+ private Date creationTime = new Date();
+
+ public enum State {
+ TASK_STAGING, TASK_RUNNING, TASK_FINISHED, TASK_KILLED, TASK_LOST, TASK_FAILED, TASK_ERROR, TASK_DROPPED, TASK_GONE, TASK_GONE_BY_OPERATOR, TASK_UNREACHABLE, TASK_UNKNOWN
+ }
+
+ public enum Source {
+ CLOUD_SCHEDULER, CLOUD_EXECUTOR, LITE_EXECUTOR
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/AppConfigurationException.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/AppConfigurationException.java
new file mode 100644
index 0000000..85b201c
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/AppConfigurationException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.exception;
+
+/**
+ * 作业App配置异常.
+ *
+ * @author caohao
+ */
+public final class AppConfigurationException extends RuntimeException {
+
+ private static final long serialVersionUID = -1466479389299512371L;
+
+ public AppConfigurationException(final String errorMessage, final Object... args) {
+ super(String.format(errorMessage, args));
+ }
+
+ public AppConfigurationException(final Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/ExceptionUtil.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/ExceptionUtil.java
new file mode 100644
index 0000000..c044e26
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/ExceptionUtil.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.exception;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * 异常处理工具类.
+ *
+ * @author caohao
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class ExceptionUtil {
+
+ /**
+ * 将Throwable异常转换为字符串.
+ *
+ * @param cause 需要转换的异常
+ * @return 转换后的异常字符串
+ */
+ public static String transform(final Throwable cause) {
+ if (null == cause) {
+ return "";
+ }
+ StringWriter result = new StringWriter();
+ try (PrintWriter writer = new PrintWriter(result)) {
+ cause.printStackTrace(writer);
+ }
+ return result.toString();
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/JobConfigurationException.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/JobConfigurationException.java
new file mode 100644
index 0000000..c49b907
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/JobConfigurationException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.exception;
+
+/**
+ * 作业配置异常.
+ *
+ * @author zhangliang
+ */
+public final class JobConfigurationException extends RuntimeException {
+
+ private static final long serialVersionUID = 3244988974343209468L;
+
+ public JobConfigurationException(final String errorMessage, final Object... args) {
+ super(String.format(errorMessage, args));
+ }
+
+ public JobConfigurationException(final Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/JobExecutionEnvironmentException.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/JobExecutionEnvironmentException.java
new file mode 100644
index 0000000..d0f7955
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/JobExecutionEnvironmentException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.exception;
+
+/**
+ * 作业执行环境异常.
+ *
+ * @author zhangliang
+ */
+public final class JobExecutionEnvironmentException extends Exception {
+
+ private static final long serialVersionUID = -6670738108926897433L;
+
+ public JobExecutionEnvironmentException(final String errorMessage, final Object... args) {
+ super(String.format(errorMessage, args));
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/JobStatisticException.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/JobStatisticException.java
new file mode 100644
index 0000000..f55f105
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/JobStatisticException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.exception;
+
+/**
+ * 作业统计异常.
+ *
+ * @author liguangyun
+ */
+public final class JobStatisticException extends RuntimeException {
+
+ private static final long serialVersionUID = -2502533914008085601L;
+
+ public JobStatisticException(final Exception ex) {
+ super(ex);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/JobSystemException.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/JobSystemException.java
new file mode 100644
index 0000000..432fa92
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/exception/JobSystemException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.exception;
+
+/**
+ * 作业系统异常.
+ *
+ * @author zhangliang
+ */
+public final class JobSystemException extends RuntimeException {
+
+ private static final long serialVersionUID = 5018901344199973515L;
+
+ public JobSystemException(final String errorMessage, final Object... args) {
+ super(String.format(errorMessage, args));
+ }
+
+ public JobSystemException(final Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/AbstractElasticJobExecutor.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/AbstractElasticJobExecutor.java
new file mode 100644
index 0000000..32d8e6e
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/AbstractElasticJobExecutor.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor;
+
+import io.elasticjob.cloud.api.ShardingContext;
+import io.elasticjob.cloud.exception.ExceptionUtil;
+import io.elasticjob.cloud.executor.handler.JobExceptionHandler;
+import io.elasticjob.cloud.config.JobRootConfiguration;
+import io.elasticjob.cloud.event.type.JobExecutionEvent;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+import io.elasticjob.cloud.exception.JobExecutionEnvironmentException;
+import io.elasticjob.cloud.exception.JobSystemException;
+import io.elasticjob.cloud.executor.handler.ExecutorServiceHandler;
+import io.elasticjob.cloud.executor.handler.ExecutorServiceHandlerRegistry;
+import io.elasticjob.cloud.executor.handler.JobProperties;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * 弹性化分布式作业执行器.
+ *
+ * @author zhangliang
+ */
+@Slf4j
+public abstract class AbstractElasticJobExecutor {
+
+ @Getter(AccessLevel.PROTECTED)
+ private final JobFacade jobFacade;
+
+ @Getter(AccessLevel.PROTECTED)
+ private final JobRootConfiguration jobRootConfig;
+
+ private final String jobName;
+
+ private final ExecutorService executorService;
+
+ private final JobExceptionHandler jobExceptionHandler;
+
+ private final Map<Integer, String> itemErrorMessages;
+
+ protected AbstractElasticJobExecutor(final JobFacade jobFacade) {
+ this.jobFacade = jobFacade;
+ jobRootConfig = jobFacade.loadJobRootConfiguration(true);
+ jobName = jobRootConfig.getTypeConfig().getCoreConfig().getJobName();
+ executorService = ExecutorServiceHandlerRegistry.getExecutorServiceHandler(jobName, (ExecutorServiceHandler) getHandler(JobProperties.JobPropertiesEnum.EXECUTOR_SERVICE_HANDLER));
+ jobExceptionHandler = (JobExceptionHandler) getHandler(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER);
+ itemErrorMessages = new ConcurrentHashMap<>(jobRootConfig.getTypeConfig().getCoreConfig().getShardingTotalCount(), 1);
+ }
+
+ private Object getHandler(final JobProperties.JobPropertiesEnum jobPropertiesEnum) {
+ String handlerClassName = jobRootConfig.getTypeConfig().getCoreConfig().getJobProperties().get(jobPropertiesEnum);
+ try {
+ Class<?> handlerClass = Class.forName(handlerClassName);
+ if (jobPropertiesEnum.getClassType().isAssignableFrom(handlerClass)) {
+ return handlerClass.newInstance();
+ }
+ return getDefaultHandler(jobPropertiesEnum, handlerClassName);
+ } catch (final ReflectiveOperationException ex) {
+ return getDefaultHandler(jobPropertiesEnum, handlerClassName);
+ }
+ }
+
+ private Object getDefaultHandler(final JobProperties.JobPropertiesEnum jobPropertiesEnum, final String handlerClassName) {
+ log.warn("Cannot instantiation class '{}', use default '{}' class.", handlerClassName, jobPropertiesEnum.getKey());
+ try {
+ return Class.forName(jobPropertiesEnum.getDefaultValue()).newInstance();
+ } catch (final ClassNotFoundException | InstantiationException | IllegalAccessException e) {
+ throw new JobSystemException(e);
+ }
+ }
+
+ /**
+ * 执行作业.
+ */
+ public final void execute() {
+ try {
+ jobFacade.checkJobExecutionEnvironment();
+ } catch (final JobExecutionEnvironmentException cause) {
+ jobExceptionHandler.handleException(jobName, cause);
+ }
+ ShardingContexts shardingContexts = jobFacade.getShardingContexts();
+ if (shardingContexts.isAllowSendJobEvent()) {
+ jobFacade.postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_STAGING, String.format("Job '%s' execute begin.", jobName));
+ }
+ if (jobFacade.misfireIfRunning(shardingContexts.getShardingItemParameters().keySet())) {
+ if (shardingContexts.isAllowSendJobEvent()) {
+ jobFacade.postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_FINISHED, String.format(
+ "Previous job '%s' - shardingItems '%s' is still running, misfired job will start after previous job completed.", jobName,
+ shardingContexts.getShardingItemParameters().keySet()));
+ }
+ return;
+ }
+ try {
+ jobFacade.beforeJobExecuted(shardingContexts);
+ //CHECKSTYLE:OFF
+ } catch (final Throwable cause) {
+ //CHECKSTYLE:ON
+ jobExceptionHandler.handleException(jobName, cause);
+ }
+ execute(shardingContexts, JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER);
+ while (jobFacade.isExecuteMisfired(shardingContexts.getShardingItemParameters().keySet())) {
+ jobFacade.clearMisfire(shardingContexts.getShardingItemParameters().keySet());
+ execute(shardingContexts, JobExecutionEvent.ExecutionSource.MISFIRE);
+ }
+ jobFacade.failoverIfNecessary();
+ try {
+ jobFacade.afterJobExecuted(shardingContexts);
+ //CHECKSTYLE:OFF
+ } catch (final Throwable cause) {
+ //CHECKSTYLE:ON
+ jobExceptionHandler.handleException(jobName, cause);
+ }
+ }
+
+ private void execute(final ShardingContexts shardingContexts, final JobExecutionEvent.ExecutionSource executionSource) {
+ if (shardingContexts.getShardingItemParameters().isEmpty()) {
+ if (shardingContexts.isAllowSendJobEvent()) {
+ jobFacade.postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_FINISHED, String.format("Sharding item for job '%s' is empty.", jobName));
+ }
+ return;
+ }
+ jobFacade.registerJobBegin(shardingContexts);
+ String taskId = shardingContexts.getTaskId();
+ if (shardingContexts.isAllowSendJobEvent()) {
+ jobFacade.postJobStatusTraceEvent(taskId, JobStatusTraceEvent.State.TASK_RUNNING, "");
+ }
+ try {
+ process(shardingContexts, executionSource);
+ } finally {
+ // TODO 考虑增加作业失败的状态,并且考虑如何处理作业失败的整体回路
+ jobFacade.registerJobCompleted(shardingContexts);
+ if (itemErrorMessages.isEmpty()) {
+ if (shardingContexts.isAllowSendJobEvent()) {
+ jobFacade.postJobStatusTraceEvent(taskId, JobStatusTraceEvent.State.TASK_FINISHED, "");
+ }
+ } else {
+ if (shardingContexts.isAllowSendJobEvent()) {
+ jobFacade.postJobStatusTraceEvent(taskId, JobStatusTraceEvent.State.TASK_ERROR, itemErrorMessages.toString());
+ }
+ }
+ }
+ }
+
+ private void process(final ShardingContexts shardingContexts, final JobExecutionEvent.ExecutionSource executionSource) {
+ Collection<Integer> items = shardingContexts.getShardingItemParameters().keySet();
+ if (1 == items.size()) {
+ int item = shardingContexts.getShardingItemParameters().keySet().iterator().next();
+ JobExecutionEvent jobExecutionEvent = new JobExecutionEvent(shardingContexts.getTaskId(), jobName, executionSource, item);
+ process(shardingContexts, item, jobExecutionEvent);
+ return;
+ }
+ final CountDownLatch latch = new CountDownLatch(items.size());
+ for (final int each : items) {
+ final JobExecutionEvent jobExecutionEvent = new JobExecutionEvent(shardingContexts.getTaskId(), jobName, executionSource, each);
+ if (executorService.isShutdown()) {
+ return;
+ }
+ executorService.submit(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ process(shardingContexts, each, jobExecutionEvent);
+ } finally {
+ latch.countDown();
+ }
+ }
+ });
+ }
+ try {
+ latch.await();
+ } catch (final InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private void process(final ShardingContexts shardingContexts, final int item, final JobExecutionEvent startEvent) {
+ if (shardingContexts.isAllowSendJobEvent()) {
+ jobFacade.postJobExecutionEvent(startEvent);
+ }
+ log.trace("Job '{}' executing, item is: '{}'.", jobName, item);
+ JobExecutionEvent completeEvent;
+ try {
+ process(new ShardingContext(shardingContexts, item));
+ completeEvent = startEvent.executionSuccess();
+ log.trace("Job '{}' executed, item is: '{}'.", jobName, item);
+ if (shardingContexts.isAllowSendJobEvent()) {
+ jobFacade.postJobExecutionEvent(completeEvent);
+ }
+ // CHECKSTYLE:OFF
+ } catch (final Throwable cause) {
+ // CHECKSTYLE:ON
+ completeEvent = startEvent.executionFailure(cause);
+ jobFacade.postJobExecutionEvent(completeEvent);
+ itemErrorMessages.put(item, ExceptionUtil.transform(cause));
+ jobExceptionHandler.handleException(jobName, cause);
+ }
+ }
+
+ protected abstract void process(ShardingContext shardingContext);
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/JobExecutorFactory.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/JobExecutorFactory.java
new file mode 100644
index 0000000..116ae51
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/JobExecutorFactory.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor;
+
+import io.elasticjob.cloud.api.ElasticJob;
+import io.elasticjob.cloud.api.dataflow.DataflowJob;
+import io.elasticjob.cloud.api.simple.SimpleJob;
+import io.elasticjob.cloud.executor.type.DataflowJobExecutor;
+import io.elasticjob.cloud.exception.JobConfigurationException;
+import io.elasticjob.cloud.executor.type.ScriptJobExecutor;
+import io.elasticjob.cloud.executor.type.SimpleJobExecutor;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+/**
+ * 作业执行器工厂.
+ *
+ * @author zhangliang
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class JobExecutorFactory {
+
+ /**
+ * 获取作业执行器.
+ *
+ * @param elasticJob 分布式弹性作业
+ * @param jobFacade 作业内部服务门面服务
+ * @return 作业执行器
+ */
+ @SuppressWarnings("unchecked")
+ public static AbstractElasticJobExecutor getJobExecutor(final ElasticJob elasticJob, final JobFacade jobFacade) {
+ if (null == elasticJob) {
+ return new ScriptJobExecutor(jobFacade);
+ }
+ if (elasticJob instanceof SimpleJob) {
+ return new SimpleJobExecutor((SimpleJob) elasticJob, jobFacade);
+ }
+ if (elasticJob instanceof DataflowJob) {
+ return new DataflowJobExecutor((DataflowJob) elasticJob, jobFacade);
+ }
+ throw new JobConfigurationException("Cannot support job type '%s'", elasticJob.getClass().getCanonicalName());
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/JobFacade.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/JobFacade.java
new file mode 100644
index 0000000..081e629
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/JobFacade.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor;
+
+import io.elasticjob.cloud.config.JobRootConfiguration;
+import io.elasticjob.cloud.event.type.JobExecutionEvent;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+import io.elasticjob.cloud.exception.JobExecutionEnvironmentException;
+
+import java.util.Collection;
+
+/**
+ * 作业内部服务门面服务.
+ *
+ * @author zhangliang
+ */
+public interface JobFacade {
+
+ /**
+ * 读取作业配置.
+ *
+ * @param fromCache 是否从缓存中读取
+ * @return 作业配置
+ */
+ JobRootConfiguration loadJobRootConfiguration(boolean fromCache);
+
+ /**
+ * 检查作业执行环境.
+ *
+ * @throws JobExecutionEnvironmentException 作业执行环境异常
+ */
+ void checkJobExecutionEnvironment() throws JobExecutionEnvironmentException;
+
+ /**
+ * 如果需要失效转移, 则执行作业失效转移.
+ */
+ void failoverIfNecessary();
+
+ /**
+ * 注册作业启动信息.
+ *
+ * @param shardingContexts 分片上下文
+ */
+ void registerJobBegin(ShardingContexts shardingContexts);
+
+ /**
+ * 注册作业完成信息.
+ *
+ * @param shardingContexts 分片上下文
+ */
+ void registerJobCompleted(ShardingContexts shardingContexts);
+
+ /**
+ * 获取当前作业服务器的分片上下文.
+ *
+ * @return 分片上下文
+ */
+ ShardingContexts getShardingContexts();
+
+ /**
+ * 设置任务被错过执行的标记.
+ *
+ * @param shardingItems 需要设置错过执行的任务分片项
+ * @return 是否满足misfire条件
+ */
+ boolean misfireIfRunning(Collection<Integer> shardingItems);
+
+ /**
+ * 清除任务被错过执行的标记.
+ *
+ * @param shardingItems 需要清除错过执行的任务分片项
+ */
+ void clearMisfire(Collection<Integer> shardingItems);
+
+ /**
+ * 判断作业是否需要执行错过的任务.
+ *
+ * @param shardingItems 任务分片项集合
+ * @return 作业是否需要执行错过的任务
+ */
+ boolean isExecuteMisfired(Collection<Integer> shardingItems);
+
+ /**
+ * 判断作业是否符合继续运行的条件.
+ *
+ * <p>如果作业停止或需要重分片或非流式处理则作业将不会继续运行.</p>
+ *
+ * @return 作业是否符合继续运行的条件
+ */
+ boolean isEligibleForJobRunning();
+
+ /**判断是否需要重分片.
+ *
+ * @return 是否需要重分片
+ */
+ boolean isNeedSharding();
+
+ /**
+ * 作业执行前的执行的方法.
+ *
+ * @param shardingContexts 分片上下文
+ */
+ void beforeJobExecuted(ShardingContexts shardingContexts);
+
+ /**
+ * 作业执行后的执行的方法.
+ *
+ * @param shardingContexts 分片上下文
+ */
+ void afterJobExecuted(ShardingContexts shardingContexts);
+
+ /**
+ * 发布执行事件.
+ *
+ * @param jobExecutionEvent 作业执行事件
+ */
+ void postJobExecutionEvent(JobExecutionEvent jobExecutionEvent);
+
+ /**
+ * 发布作业状态追踪事件.
+ *
+ * @param taskId 作业Id
+ * @param state 作业执行状态
+ * @param message 作业执行消息
+ */
+ void postJobStatusTraceEvent(String taskId, JobStatusTraceEvent.State state, String message);
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/ShardingContexts.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/ShardingContexts.java
new file mode 100644
index 0000000..ce72f6b
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/ShardingContexts.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * 分片上下文集合.
+ *
+ * @author zhangliang
+ */
+@RequiredArgsConstructor
+@Getter
+@ToString
+public final class ShardingContexts implements Serializable {
+
+ private static final long serialVersionUID = -4585977349142082152L;
+
+ /**
+ * 作业任务ID.
+ */
+ private final String taskId;
+
+ /**
+ * 作业名称.
+ */
+ private final String jobName;
+
+ /**
+ * 分片总数.
+ */
+ private final int shardingTotalCount;
+
+ /**
+ * 作业自定义参数.
+ * 可以配置多个相同的作业, 但是用不同的参数作为不同的调度实例.
+ */
+ private final String jobParameter;
+
+ /**
+ * 分配于本作业实例的分片项和参数的Map.
+ */
+ private final Map<Integer, String> shardingItemParameters;
+
+ /**
+ * 作业事件采样统计数.
+ */
+ private int jobEventSamplingCount;
+
+ /**
+ * 当前作业事件采样统计数.
+ */
+ @Setter
+ private int currentJobEventSamplingCount;
+
+ /**
+ * 是否允许可以发送作业事件.
+ */
+ @Setter
+ private boolean allowSendJobEvent = true;
+
+ public ShardingContexts(final String taskId, final String jobName, final int shardingTotalCount, final String jobParameter,
+ final Map<Integer, String> shardingItemParameters, final int jobEventSamplingCount) {
+ this.taskId = taskId;
+ this.jobName = jobName;
+ this.shardingTotalCount = shardingTotalCount;
+ this.jobParameter = jobParameter;
+ this.shardingItemParameters = shardingItemParameters;
+ this.jobEventSamplingCount = jobEventSamplingCount;
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/ExecutorServiceHandler.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/ExecutorServiceHandler.java
new file mode 100644
index 0000000..8e15785
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/ExecutorServiceHandler.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.handler;
+
+import java.util.concurrent.ExecutorService;
+
+/**
+ * 线程池服务处理器.
+ *
+ * <p>用于作业内部的线程池处理使用.</p>
+ *
+ * @author zhangliang
+ */
+public interface ExecutorServiceHandler {
+
+ /**
+ * 创建线程池服务对象.
+ *
+ * @param jobName 作业名
+ *
+ * @return 线程池服务对象
+ */
+ ExecutorService createExecutorService(final String jobName);
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/ExecutorServiceHandlerRegistry.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/ExecutorServiceHandlerRegistry.java
new file mode 100644
index 0000000..b2549ce
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/ExecutorServiceHandlerRegistry.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.handler;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * 线程池服务处理器注册表.
+ *
+ * @author zhangliang
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class ExecutorServiceHandlerRegistry {
+
+ private static final Map<String, ExecutorService> REGISTRY = new HashMap<>();
+
+ /**
+ * 获取线程池服务.
+ *
+ * @param jobName 作业名称
+ * @param executorServiceHandler 线程池服务处理器
+ * @return 线程池服务
+ */
+ public static synchronized ExecutorService getExecutorServiceHandler(final String jobName, final ExecutorServiceHandler executorServiceHandler) {
+ if (!REGISTRY.containsKey(jobName)) {
+ REGISTRY.put(jobName, executorServiceHandler.createExecutorService(jobName));
+ }
+ return REGISTRY.get(jobName);
+ }
+
+ /**
+ * 从注册表中删除该作业线程池服务.
+ *
+ * @param jobName 作业名称
+ */
+ public static synchronized void remove(final String jobName) {
+ REGISTRY.remove(jobName);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/JobExceptionHandler.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/JobExceptionHandler.java
new file mode 100644
index 0000000..f60e1a9
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/JobExceptionHandler.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.handler;
+
+/**
+ * 作业异常处理器.
+ *
+ * @author zhangliang
+ */
+public interface JobExceptionHandler {
+
+ /**
+ * 处理作业异常.
+ *
+ * @param jobName 作业名称
+ * @param cause 异常原因
+ */
+ void handleException(String jobName, Throwable cause);
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/JobProperties.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/JobProperties.java
new file mode 100644
index 0000000..fd8bed2
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/JobProperties.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.handler;
+
+import io.elasticjob.cloud.executor.handler.impl.DefaultJobExceptionHandler;
+import io.elasticjob.cloud.util.json.GsonFactory;
+import io.elasticjob.cloud.executor.handler.impl.DefaultExecutorServiceHandler;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.RequiredArgsConstructor;
+
+import java.util.EnumMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 作业属性配置.
+ *
+ * @author zhangliang
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+public final class JobProperties {
+
+ private EnumMap<JobPropertiesEnum, String> map = new EnumMap<>(JobPropertiesEnum.class);
+
+ /**
+ * 设置作业属性.
+ *
+ * @param key 属性键
+ * @param value 属性值
+ */
+ public void put(final String key, final String value) {
+ JobPropertiesEnum jobPropertiesEnum = JobPropertiesEnum.from(key);
+ if (null == jobPropertiesEnum || null == value) {
+ return;
+ }
+ map.put(jobPropertiesEnum, value);
+ }
+
+ /**
+ * 获取作业属性.
+ *
+ * @param jobPropertiesEnum 作业属性枚举
+ * @return 属性值
+ */
+ public String get(final JobPropertiesEnum jobPropertiesEnum) {
+ return map.containsKey(jobPropertiesEnum) ? map.get(jobPropertiesEnum) : jobPropertiesEnum.getDefaultValue();
+ }
+
+ /**
+ * 获取所有键.
+ *
+ * @return 键集合
+ */
+ public String json() {
+ Map<String, String> jsonMap = new LinkedHashMap<>(JobPropertiesEnum.values().length, 1);
+ for (JobPropertiesEnum each : JobPropertiesEnum.values()) {
+ jsonMap.put(each.getKey(), get(each));
+ }
+ return GsonFactory.getGson().toJson(jsonMap);
+ }
+
+ /**
+ * 作业属性枚举.
+ */
+ @RequiredArgsConstructor
+ @Getter
+ public enum JobPropertiesEnum {
+
+ /**
+ * 作业异常处理器.
+ */
+ JOB_EXCEPTION_HANDLER("job_exception_handler", JobExceptionHandler.class, DefaultJobExceptionHandler.class.getCanonicalName()),
+
+ /**
+ * 线程池服务处理器.
+ */
+ EXECUTOR_SERVICE_HANDLER("executor_service_handler", ExecutorServiceHandler.class, DefaultExecutorServiceHandler.class.getCanonicalName());
+
+ private final String key;
+
+ private final Class<?> classType;
+
+ private final String defaultValue;
+
+ /**
+ * 通过属性键获取枚举.
+ *
+ * @param key 属性键
+ * @return 枚举
+ */
+ public static JobPropertiesEnum from(final String key) {
+ for (JobPropertiesEnum each : JobPropertiesEnum.values()) {
+ if (each.getKey().equals(key)) {
+ return each;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/impl/DefaultExecutorServiceHandler.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/impl/DefaultExecutorServiceHandler.java
new file mode 100644
index 0000000..000e934
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/impl/DefaultExecutorServiceHandler.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.handler.impl;
+
+import io.elasticjob.cloud.util.concurrent.ExecutorServiceObject;
+import io.elasticjob.cloud.executor.handler.ExecutorServiceHandler;
+
+import java.util.concurrent.ExecutorService;
+
+/**
+ * 默认线程池服务处理器.
+ *
+ * @author zhangliang
+ */
+public final class DefaultExecutorServiceHandler implements ExecutorServiceHandler {
+
+ @Override
+ public ExecutorService createExecutorService(final String jobName) {
+ return new ExecutorServiceObject("inner-job-" + jobName, Runtime.getRuntime().availableProcessors() * 2).createExecutorService();
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/impl/DefaultJobExceptionHandler.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/impl/DefaultJobExceptionHandler.java
new file mode 100644
index 0000000..43c56b6
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/handler/impl/DefaultJobExceptionHandler.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.handler.impl;
+
+import io.elasticjob.cloud.executor.handler.JobExceptionHandler;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 默认作业异常处理器.
+ *
+ * @author zhangliang
+ */
+@Slf4j
+public final class DefaultJobExceptionHandler implements JobExceptionHandler {
+
+ @Override
+ public void handleException(final String jobName, final Throwable cause) {
+ log.error(String.format("Job '%s' exception occur in job processing", jobName), cause);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/type/DataflowJobExecutor.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/type/DataflowJobExecutor.java
new file mode 100644
index 0000000..d45ea87
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/type/DataflowJobExecutor.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.type;
+
+import io.elasticjob.cloud.api.ShardingContext;
+import io.elasticjob.cloud.api.dataflow.DataflowJob;
+import io.elasticjob.cloud.config.dataflow.DataflowJobConfiguration;
+import io.elasticjob.cloud.executor.AbstractElasticJobExecutor;
+import io.elasticjob.cloud.executor.JobFacade;
+
+import java.util.List;
+
+/**
+ * 数据流作业执行器.
+ *
+ * @author zhangliang
+ */
+public final class DataflowJobExecutor extends AbstractElasticJobExecutor {
+
+ private final DataflowJob<Object> dataflowJob;
+
+ public DataflowJobExecutor(final DataflowJob<Object> dataflowJob, final JobFacade jobFacade) {
+ super(jobFacade);
+ this.dataflowJob = dataflowJob;
+ }
+
+ @Override
+ protected void process(final ShardingContext shardingContext) {
+ DataflowJobConfiguration dataflowConfig = (DataflowJobConfiguration) getJobRootConfig().getTypeConfig();
+ if (dataflowConfig.isStreamingProcess()) {
+ streamingExecute(shardingContext);
+ } else {
+ oneOffExecute(shardingContext);
+ }
+ }
+
+ private void streamingExecute(final ShardingContext shardingContext) {
+ List<Object> data = fetchData(shardingContext);
+ while (null != data && !data.isEmpty()) {
+ processData(shardingContext, data);
+ if (!getJobFacade().isEligibleForJobRunning()) {
+ break;
+ }
+ data = fetchData(shardingContext);
+ }
+ }
+
+ private void oneOffExecute(final ShardingContext shardingContext) {
+ List<Object> data = fetchData(shardingContext);
+ if (null != data && !data.isEmpty()) {
+ processData(shardingContext, data);
+ }
+ }
+
+ private List<Object> fetchData(final ShardingContext shardingContext) {
+ return dataflowJob.fetchData(shardingContext);
+ }
+
+ private void processData(final ShardingContext shardingContext, final List<Object> data) {
+ dataflowJob.processData(shardingContext, data);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/type/ScriptJobExecutor.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/type/ScriptJobExecutor.java
new file mode 100644
index 0000000..27f177f
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/type/ScriptJobExecutor.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.type;
+
+import com.google.common.base.Strings;
+import io.elasticjob.cloud.api.ShardingContext;
+import io.elasticjob.cloud.config.script.ScriptJobConfiguration;
+import io.elasticjob.cloud.exception.JobConfigurationException;
+import io.elasticjob.cloud.executor.AbstractElasticJobExecutor;
+import io.elasticjob.cloud.executor.JobFacade;
+import io.elasticjob.cloud.util.json.GsonFactory;
+import org.apache.commons.exec.CommandLine;
+import org.apache.commons.exec.DefaultExecutor;
+
+import java.io.IOException;
+
+/**
+ * 脚本作业执行器.
+ *
+ * @author zhangliang
+ * @author caohao
+ */
+public final class ScriptJobExecutor extends AbstractElasticJobExecutor {
+
+ public ScriptJobExecutor(final JobFacade jobFacade) {
+ super(jobFacade);
+ }
+
+ @Override
+ protected void process(final ShardingContext shardingContext) {
+ final String scriptCommandLine = ((ScriptJobConfiguration) getJobRootConfig().getTypeConfig()).getScriptCommandLine();
+ if (Strings.isNullOrEmpty(scriptCommandLine)) {
+ throw new JobConfigurationException("Cannot find script command line for job '%s', job is not executed.", shardingContext.getJobName());
+ }
+ executeScript(shardingContext, scriptCommandLine);
+ }
+
+ private void executeScript(final ShardingContext shardingContext, final String scriptCommandLine) {
+ CommandLine commandLine = CommandLine.parse(scriptCommandLine);
+ commandLine.addArgument(GsonFactory.getGson().toJson(shardingContext), false);
+ try {
+ new DefaultExecutor().execute(commandLine);
+ } catch (final IOException ex) {
+ throw new JobConfigurationException("Execute script failure.", ex);
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/type/SimpleJobExecutor.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/type/SimpleJobExecutor.java
new file mode 100644
index 0000000..cd49601
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/executor/type/SimpleJobExecutor.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.type;
+
+import io.elasticjob.cloud.api.ShardingContext;
+import io.elasticjob.cloud.api.simple.SimpleJob;
+import io.elasticjob.cloud.executor.AbstractElasticJobExecutor;
+import io.elasticjob.cloud.executor.JobFacade;
+
+/**
+ * 简单作业执行器.
+ *
+ * @author zhangliang
+ */
+public final class SimpleJobExecutor extends AbstractElasticJobExecutor {
+
+ private final SimpleJob simpleJob;
+
+ public SimpleJobExecutor(final SimpleJob simpleJob, final JobFacade jobFacade) {
+ super(jobFacade);
+ this.simpleJob = simpleJob;
+ }
+
+ @Override
+ protected void process(final ShardingContext shardingContext) {
+ simpleJob.execute(shardingContext);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/base/CoordinatorRegistryCenter.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/base/CoordinatorRegistryCenter.java
new file mode 100644
index 0000000..564e710
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/base/CoordinatorRegistryCenter.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.base;
+
+import java.util.List;
+
+/**
+ * 用于协调分布式服务的注册中心.
+ *
+ * @author zhangliang
+ */
+public interface CoordinatorRegistryCenter extends RegistryCenter {
+
+ /**
+ * 直接从注册中心而非本地缓存获取数据.
+ *
+ * @param key 键
+ * @return 值
+ */
+ String getDirectly(String key);
+
+ /**
+ * 获取子节点名称集合.
+ *
+ * @param key 键
+ * @return 子节点名称集合
+ */
+ List<String> getChildrenKeys(String key);
+
+ /**
+ * 获取子节点数量.
+ *
+ * @param key 键
+ * @return 子节点数量
+ */
+ int getNumChildren(String key);
+
+ /**
+ * 持久化临时注册数据.
+ *
+ * @param key 键
+ * @param value 值
+ */
+ void persistEphemeral(String key, String value);
+
+ /**
+ * 持久化顺序注册数据.
+ *
+ * @param key 键
+ * @param value 值
+ * @return 包含10位顺序数字的znode名称
+ */
+ String persistSequential(String key, String value);
+
+ /**
+ * 持久化临时顺序注册数据.
+ *
+ * @param key 键
+ */
+ void persistEphemeralSequential(String key);
+
+ /**
+ * 添加本地缓存.
+ *
+ * @param cachePath 需加入缓存的路径
+ */
+ void addCacheData(String cachePath);
+
+ /**
+ * 释放本地缓存.
+ *
+ * @param cachePath 需释放缓存的路径
+ */
+ void evictCacheData(String cachePath);
+
+ /**
+ * 获取注册中心数据缓存对象.
+ *
+ * @param cachePath 缓存的节点路径
+ * @return 注册中心数据缓存对象
+ */
+ Object getRawCache(String cachePath);
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/base/ElectionCandidate.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/base/ElectionCandidate.java
new file mode 100644
index 0000000..689850a
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/base/ElectionCandidate.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.base;
+
+/**
+ * 选举候选人.
+ * 保证{@link #startLeadership()}与{@link #stopLeadership()}方法在同一个线程内交替运行,
+ * 且不会出现并发执行的情况.
+ *
+ * @author gaohongtao
+ */
+public interface ElectionCandidate {
+
+ /**
+ * 开始领导状态.
+ * @throws Exception 抛出的异常
+ */
+ void startLeadership() throws Exception;
+
+ /**
+ * 终止领导状态.
+ * 实现该方法时不应该抛出任何异常
+ */
+ void stopLeadership();
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/base/RegistryCenter.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/base/RegistryCenter.java
new file mode 100644
index 0000000..30492f9
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/base/RegistryCenter.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.base;
+
+/**
+ * 注册中心.
+ *
+ * @author zhangliang
+ */
+public interface RegistryCenter {
+
+ /**
+ * 初始化注册中心.
+ */
+ void init();
+
+ /**
+ * 关闭注册中心.
+ */
+ void close();
+
+ /**
+ * 获取注册数据.
+ *
+ * @param key 键
+ * @return 值
+ */
+ String get(String key);
+
+ /**
+ * 获取数据是否存在.
+ *
+ * @param key 键
+ * @return 数据是否存在
+ */
+ boolean isExisted(String key);
+
+ /**
+ * 持久化注册数据.
+ *
+ * @param key 键
+ * @param value 值
+ */
+ void persist(String key, String value);
+
+ /**
+ * 更新注册数据.
+ *
+ * @param key 键
+ * @param value 值
+ */
+ void update(String key, String value);
+
+ /**
+ * 删除注册数据.
+ *
+ * @param key 键
+ */
+ void remove(String key);
+
+ /**
+ * 获取注册中心当前时间.
+ *
+ * @param key 用于获取时间的键
+ * @return 注册中心当前时间
+ */
+ long getRegistryCenterTime(String key);
+
+ /**
+ * 直接获取操作注册中心的原生客户端.
+ * 如:Zookeeper或Redis等原生客户端.
+ *
+ * @return 注册中心的原生客户端
+ */
+ Object getRawClient();
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/exception/RegException.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/exception/RegException.java
new file mode 100644
index 0000000..6ee1f41
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/exception/RegException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.exception;
+
+/**
+ * 注册中心异常.
+ *
+ * @author zhangliang
+ */
+public final class RegException extends RuntimeException {
+
+ private static final long serialVersionUID = -6417179023552012152L;
+
+ public RegException(final String errorMessage, final Object... args) {
+ super(String.format(errorMessage, args));
+ }
+
+ public RegException(final Exception cause) {
+ super(cause);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/exception/RegExceptionHandler.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/exception/RegExceptionHandler.java
new file mode 100644
index 0000000..d8882c0
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/exception/RegExceptionHandler.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.exception;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.zookeeper.KeeperException.ConnectionLossException;
+import org.apache.zookeeper.KeeperException.NoNodeException;
+import org.apache.zookeeper.KeeperException.NodeExistsException;
+
+/**
+ * 注册中心异常处理类.
+ *
+ * @author zhangliang
+ */
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class RegExceptionHandler {
+
+ /**
+ * 处理异常.
+ *
+ * <p>处理掉中断和连接失效异常并继续抛注册中心.</p>
+ *
+ * @param cause 待处理异常.
+ */
+ public static void handleException(final Exception cause) {
+ if (null == cause) {
+ return;
+ }
+ if (isIgnoredException(cause) || null != cause.getCause() && isIgnoredException(cause.getCause())) {
+ log.debug("Elastic job: ignored exception for: {}", cause.getMessage());
+ } else if (cause instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ } else {
+ throw new RegException(cause);
+ }
+ }
+
+ private static boolean isIgnoredException(final Throwable cause) {
+ return cause instanceof ConnectionLossException || cause instanceof NoNodeException || cause instanceof NodeExistsException;
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperConfiguration.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperConfiguration.java
new file mode 100644
index 0000000..20f802b
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperConfiguration.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.zookeeper;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+
+/**
+ * 基于Zookeeper的注册中心配置.
+ *
+ * @author zhangliang
+ * @author caohao
+ */
+@Getter
+@Setter
+@RequiredArgsConstructor
+public final class ZookeeperConfiguration {
+
+ /**
+ * 连接Zookeeper服务器的列表.
+ * 包括IP地址和端口号.
+ * 多个地址用逗号分隔.
+ * 如: host1:2181,host2:2181
+ */
+ private final String serverLists;
+
+ /**
+ * 命名空间.
+ */
+ private final String namespace;
+
+ /**
+ * 等待重试的间隔时间的初始值.
+ * 单位毫秒.
+ */
+ private int baseSleepTimeMilliseconds = 1000;
+
+ /**
+ * 等待重试的间隔时间的最大值.
+ * 单位毫秒.
+ */
+ private int maxSleepTimeMilliseconds = 3000;
+
+ /**
+ * 最大重试次数.
+ */
+ private int maxRetries = 3;
+
+ /**
+ * 会话超时时间.
+ * 单位毫秒.
+ */
+ private int sessionTimeoutMilliseconds;
+
+ /**
+ * 连接超时时间.
+ * 单位毫秒.
+ */
+ private int connectionTimeoutMilliseconds;
+
+ /**
+ * 连接Zookeeper的权限令牌.
+ * 缺省为不需要权限验证.
+ */
+ private String digest;
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperElectionService.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperElectionService.java
new file mode 100644
index 0000000..cbb68b1
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperElectionService.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.zookeeper;
+
+import io.elasticjob.cloud.reg.base.ElectionCandidate;
+import io.elasticjob.cloud.exception.JobSystemException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.leader.LeaderSelector;
+import org.apache.curator.framework.recipes.leader.LeaderSelectorListenerAdapter;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * 使用{@link LeaderSelector}实现选举服务.
+ *
+ * @author gaohongtao
+ * @author caohao
+ */
+@Slf4j
+public final class ZookeeperElectionService {
+
+ private final CountDownLatch leaderLatch = new CountDownLatch(1);
+
+ private final LeaderSelector leaderSelector;
+
+ public ZookeeperElectionService(final String identity, final CuratorFramework client, final String electionPath, final ElectionCandidate electionCandidate) {
+ leaderSelector = new LeaderSelector(client, electionPath, new LeaderSelectorListenerAdapter() {
+
+ @Override
+ public void takeLeadership(final CuratorFramework client) throws Exception {
+ log.info("Elastic job: {} has leadership", identity);
+ try {
+ electionCandidate.startLeadership();
+ leaderLatch.await();
+ log.warn("Elastic job: {} lost leadership.", identity);
+ electionCandidate.stopLeadership();
+ } catch (final JobSystemException exception) {
+ log.error("Elastic job: Starting error", exception);
+ System.exit(1);
+ }
+ }
+ });
+ leaderSelector.autoRequeue();
+ leaderSelector.setId(identity);
+ }
+
+ /**
+ * 开始选举.
+ */
+ public void start() {
+ log.debug("Elastic job: {} start to elect leadership", leaderSelector.getId());
+ leaderSelector.start();
+ }
+
+ /**
+ * 停止选举.
+ */
+ public void stop() {
+ log.info("Elastic job: stop leadership election");
+ leaderLatch.countDown();
+ try {
+ leaderSelector.close();
+ // CHECKSTYLE:OFF
+ } catch (final Exception ignored) {
+ }
+ // CHECKSTYLE:ON
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenter.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenter.java
new file mode 100644
index 0000000..90d6609
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenter.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.zookeeper;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import io.elasticjob.cloud.reg.base.CoordinatorRegistryCenter;
+import io.elasticjob.cloud.reg.exception.RegExceptionHandler;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.api.ACLProvider;
+import org.apache.curator.framework.recipes.cache.ChildData;
+import org.apache.curator.framework.recipes.cache.TreeCache;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.curator.utils.CloseableUtils;
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.ZooDefs;
+import org.apache.zookeeper.data.ACL;
+import org.apache.zookeeper.data.Stat;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 基于Zookeeper的注册中心.
+ *
+ * @author zhangliang
+ */
+@Slf4j
+public final class ZookeeperRegistryCenter implements CoordinatorRegistryCenter {
+
+ @Getter(AccessLevel.PROTECTED)
+ private ZookeeperConfiguration zkConfig;
+
+ private final Map<String, TreeCache> caches = new HashMap<>();
+
+ @Getter
+ private CuratorFramework client;
+
+ public ZookeeperRegistryCenter(final ZookeeperConfiguration zkConfig) {
+ this.zkConfig = zkConfig;
+ }
+
+ @Override
+ public void init() {
+ log.debug("Elastic job: zookeeper registry center init, server lists is: {}.", zkConfig.getServerLists());
+ CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder()
+ .connectString(zkConfig.getServerLists())
+ .retryPolicy(new ExponentialBackoffRetry(zkConfig.getBaseSleepTimeMilliseconds(), zkConfig.getMaxRetries(), zkConfig.getMaxSleepTimeMilliseconds()))
+ .namespace(zkConfig.getNamespace());
+ if (0 != zkConfig.getSessionTimeoutMilliseconds()) {
+ builder.sessionTimeoutMs(zkConfig.getSessionTimeoutMilliseconds());
+ }
+ if (0 != zkConfig.getConnectionTimeoutMilliseconds()) {
+ builder.connectionTimeoutMs(zkConfig.getConnectionTimeoutMilliseconds());
+ }
+ if (!Strings.isNullOrEmpty(zkConfig.getDigest())) {
+ builder.authorization("digest", zkConfig.getDigest().getBytes(Charsets.UTF_8))
+ .aclProvider(new ACLProvider() {
+
+ @Override
+ public List<ACL> getDefaultAcl() {
+ return ZooDefs.Ids.CREATOR_ALL_ACL;
+ }
+
+ @Override
+ public List<ACL> getAclForPath(final String path) {
+ return ZooDefs.Ids.CREATOR_ALL_ACL;
+ }
+ });
+ }
+ client = builder.build();
+ client.start();
+ try {
+ if (!client.blockUntilConnected(zkConfig.getMaxSleepTimeMilliseconds() * zkConfig.getMaxRetries(), TimeUnit.MILLISECONDS)) {
+ client.close();
+ throw new KeeperException.OperationTimeoutException();
+ }
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ }
+ }
+
+ @Override
+ public void close() {
+ for (Entry<String, TreeCache> each : caches.entrySet()) {
+ each.getValue().close();
+ }
+ waitForCacheClose();
+ CloseableUtils.closeQuietly(client);
+ }
+
+ /* TODO 等待500ms, cache先关闭再关闭client, 否则会抛异常
+ * 因为异步处理, 可能会导致client先关闭而cache还未关闭结束.
+ * 等待Curator新版本解决这个bug.
+ * BUG地址:https://issues.apache.org/jira/browse/CURATOR-157
+ */
+ private void waitForCacheClose() {
+ try {
+ Thread.sleep(500L);
+ } catch (final InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ public String get(final String key) {
+ TreeCache cache = findTreeCache(key);
+ if (null == cache) {
+ return getDirectly(key);
+ }
+ ChildData resultInCache = cache.getCurrentData(key);
+ if (null != resultInCache) {
+ return null == resultInCache.getData() ? null : new String(resultInCache.getData(), Charsets.UTF_8);
+ }
+ return getDirectly(key);
+ }
+
+ private TreeCache findTreeCache(final String key) {
+ for (Entry<String, TreeCache> entry : caches.entrySet()) {
+ if (key.startsWith(entry.getKey())) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getDirectly(final String key) {
+ try {
+ return new String(client.getData().forPath(key), Charsets.UTF_8);
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ return null;
+ }
+ }
+
+ @Override
+ public List<String> getChildrenKeys(final String key) {
+ try {
+ List<String> result = client.getChildren().forPath(key);
+ Collections.sort(result, new Comparator<String>() {
+
+ @Override
+ public int compare(final String o1, final String o2) {
+ return o2.compareTo(o1);
+ }
+ });
+ return result;
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ return Collections.emptyList();
+ }
+ }
+
+ @Override
+ public int getNumChildren(final String key) {
+ try {
+ Stat stat = client.checkExists().forPath(key);
+ if (null != stat) {
+ return stat.getNumChildren();
+ }
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ }
+ return 0;
+ }
+
+ @Override
+ public boolean isExisted(final String key) {
+ try {
+ return null != client.checkExists().forPath(key);
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ return false;
+ }
+ }
+
+ @Override
+ public void persist(final String key, final String value) {
+ try {
+ if (!isExisted(key)) {
+ client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(key, value.getBytes(Charsets.UTF_8));
+ } else {
+ update(key, value);
+ }
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ }
+ }
+
+ @Override
+ public void update(final String key, final String value) {
+ try {
+ client.inTransaction().check().forPath(key).and().setData().forPath(key, value.getBytes(Charsets.UTF_8)).and().commit();
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ }
+ }
+
+ @Override
+ public void persistEphemeral(final String key, final String value) {
+ try {
+ if (isExisted(key)) {
+ client.delete().deletingChildrenIfNeeded().forPath(key);
+ }
+ client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(key, value.getBytes(Charsets.UTF_8));
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ }
+ }
+
+ @Override
+ public String persistSequential(final String key, final String value) {
+ try {
+ return client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT_SEQUENTIAL).forPath(key, value.getBytes(Charsets.UTF_8));
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ }
+ return null;
+ }
+
+ @Override
+ public void persistEphemeralSequential(final String key) {
+ try {
+ client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(key);
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ }
+ }
+
+ @Override
+ public void remove(final String key) {
+ try {
+ client.delete().deletingChildrenIfNeeded().forPath(key);
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ }
+ }
+
+ @Override
+ public long getRegistryCenterTime(final String key) {
+ long result = 0L;
+ try {
+ persist(key, "");
+ result = client.checkExists().forPath(key).getMtime();
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ }
+ Preconditions.checkState(0L != result, "Cannot get registry center time.");
+ return result;
+ }
+
+ @Override
+ public Object getRawClient() {
+ return client;
+ }
+
+ @Override
+ public void addCacheData(final String cachePath) {
+ TreeCache cache = new TreeCache(client, cachePath);
+ try {
+ cache.start();
+ //CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ //CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ }
+ caches.put(cachePath + "/", cache);
+ }
+
+ @Override
+ public void evictCacheData(final String cachePath) {
+ TreeCache cache = caches.remove(cachePath + "/");
+ if (null != cache) {
+ cache.close();
+ }
+ }
+
+ @Override
+ public Object getRawCache(final String cachePath) {
+ return caches.get(cachePath + "/");
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/StatisticInterval.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/StatisticInterval.java
new file mode 100644
index 0000000..b438ade
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/StatisticInterval.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.statistics;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 统计时间间隔.
+ *
+ * @author liguangyun
+ */
+@Getter
+@RequiredArgsConstructor
+public enum StatisticInterval {
+
+ MINUTE("0 * * * * ?"),
+
+ HOUR("0 0 * * * ?"),
+
+ DAY("0 0 0 * * ?");
+
+ private final String cron;
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/rdb/StatisticRdbRepository.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/rdb/StatisticRdbRepository.java
new file mode 100644
index 0000000..e4d1572
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/rdb/StatisticRdbRepository.java
@@ -0,0 +1,503 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.statistics.rdb;
+
+import com.google.common.base.Optional;
+import io.elasticjob.cloud.statistics.StatisticInterval;
+import io.elasticjob.cloud.statistics.type.job.JobRegisterStatistics;
+import io.elasticjob.cloud.statistics.type.job.JobRunningStatistics;
+import io.elasticjob.cloud.statistics.type.task.TaskResultStatistics;
+import io.elasticjob.cloud.statistics.type.task.TaskRunningStatistics;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * 统计信息数据仓库,基于数据库.
+ *
+ * @author liguangyun
+ */
+@Slf4j
+public class StatisticRdbRepository {
+
+ private static final String TABLE_TASK_RESULT_STATISTICS = "TASK_RESULT_STATISTICS";
+
+ private static final String TABLE_TASK_RUNNING_STATISTICS = "TASK_RUNNING_STATISTICS";
+
+ private static final String TABLE_JOB_RUNNING_STATISTICS = "JOB_RUNNING_STATISTICS";
+
+ private static final String TABLE_JOB_REGISTER_STATISTICS = "JOB_REGISTER_STATISTICS";
+
+ private final DataSource dataSource;
+
+ /**
+ * 构造函数.
+ *
+ * @param dataSource 数据源
+ * @throws SQLException SQL异常
+ */
+ public StatisticRdbRepository(final DataSource dataSource) throws SQLException {
+ this.dataSource = dataSource;
+ initTables();
+ }
+
+ private void initTables() throws SQLException {
+ try (Connection conn = dataSource.getConnection()) {
+ createTaskResultTableIfNeeded(conn);
+ createTaskRunningTableIfNeeded(conn);
+ createJobRunningTableIfNeeded(conn);
+ createJobRegisterTableIfNeeded(conn);
+ }
+ }
+
+ private void createTaskResultTableIfNeeded(final Connection conn) throws SQLException {
+ DatabaseMetaData dbMetaData = conn.getMetaData();
+ for (StatisticInterval each : StatisticInterval.values()) {
+ try (ResultSet resultSet = dbMetaData.getTables(null, null, TABLE_TASK_RESULT_STATISTICS + "_" + each, new String[]{"TABLE"})) {
+ if (!resultSet.next()) {
+ createTaskResultTable(conn, each);
+ }
+ }
+ }
+ }
+
+ private void createTaskResultTable(final Connection conn, final StatisticInterval statisticInterval) throws SQLException {
+ String dbSchema = "CREATE TABLE `" + TABLE_TASK_RESULT_STATISTICS + "_" + statisticInterval + "` ("
+ + "`id` BIGINT NOT NULL AUTO_INCREMENT, "
+ + "`success_count` INT(11),"
+ + "`failed_count` INT(11),"
+ + "`statistics_time` TIMESTAMP NOT NULL,"
+ + "`creation_time` TIMESTAMP NOT NULL,"
+ + "PRIMARY KEY (`id`));";
+ try (PreparedStatement preparedStatement = conn.prepareStatement(dbSchema)) {
+ preparedStatement.execute();
+ }
+ }
+
+ private void createTaskRunningTableIfNeeded(final Connection conn) throws SQLException {
+ DatabaseMetaData dbMetaData = conn.getMetaData();
+ try (ResultSet resultSet = dbMetaData.getTables(null, null, TABLE_TASK_RUNNING_STATISTICS, new String[]{"TABLE"})) {
+ if (!resultSet.next()) {
+ createTaskRunningTable(conn);
+ }
+ }
+ }
+
+ private void createTaskRunningTable(final Connection conn) throws SQLException {
+ String dbSchema = "CREATE TABLE `" + TABLE_TASK_RUNNING_STATISTICS + "` ("
+ + "`id` BIGINT NOT NULL AUTO_INCREMENT, "
+ + "`running_count` INT(11),"
+ + "`statistics_time` TIMESTAMP NOT NULL,"
+ + "`creation_time` TIMESTAMP NOT NULL,"
+ + "PRIMARY KEY (`id`));";
+ try (PreparedStatement preparedStatement = conn.prepareStatement(dbSchema)) {
+ preparedStatement.execute();
+ }
+ }
+
+ private void createJobRunningTableIfNeeded(final Connection conn) throws SQLException {
+ DatabaseMetaData dbMetaData = conn.getMetaData();
+ try (ResultSet resultSet = dbMetaData.getTables(null, null, TABLE_JOB_RUNNING_STATISTICS, new String[]{"TABLE"})) {
+ if (!resultSet.next()) {
+ createJobRunningTable(conn);
+ }
+ }
+ }
+
+ private void createJobRunningTable(final Connection conn) throws SQLException {
+ String dbSchema = "CREATE TABLE `" + TABLE_JOB_RUNNING_STATISTICS + "` ("
+ + "`id` BIGINT NOT NULL AUTO_INCREMENT, "
+ + "`running_count` INT(11),"
+ + "`statistics_time` TIMESTAMP NOT NULL,"
+ + "`creation_time` TIMESTAMP NOT NULL,"
+ + "PRIMARY KEY (`id`));";
+ try (PreparedStatement preparedStatement = conn.prepareStatement(dbSchema)) {
+ preparedStatement.execute();
+ }
+ }
+
+ private void createJobRegisterTableIfNeeded(final Connection conn) throws SQLException {
+ DatabaseMetaData dbMetaData = conn.getMetaData();
+ try (ResultSet resultSet = dbMetaData.getTables(null, null, TABLE_JOB_REGISTER_STATISTICS, new String[]{"TABLE"})) {
+ if (!resultSet.next()) {
+ createJobRegisterTable(conn);
+ }
+ }
+ }
+
+ private void createJobRegisterTable(final Connection conn) throws SQLException {
+ String dbSchema = "CREATE TABLE `" + TABLE_JOB_REGISTER_STATISTICS + "` ("
+ + "`id` BIGINT NOT NULL AUTO_INCREMENT, "
+ + "`registered_count` INT(11),"
+ + "`statistics_time` TIMESTAMP NOT NULL,"
+ + "`creation_time` TIMESTAMP NOT NULL,"
+ + "PRIMARY KEY (`id`));";
+ try (PreparedStatement preparedStatement = conn.prepareStatement(dbSchema)) {
+ preparedStatement.execute();
+ }
+ }
+
+ /**
+ * 添加任务运行结果统计数据.
+ *
+ * @param taskResultStatistics 任务运行结果统计数据对象
+ * @return 添加操作是否成功
+ */
+ public boolean add(final TaskResultStatistics taskResultStatistics) {
+ boolean result = false;
+ String sql = "INSERT INTO `" + TABLE_TASK_RESULT_STATISTICS + "_" + taskResultStatistics.getStatisticInterval()
+ + "` (`success_count`, `failed_count`, `statistics_time`, `creation_time`) VALUES (?, ?, ?, ?);";
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
+ preparedStatement.setInt(1, taskResultStatistics.getSuccessCount());
+ preparedStatement.setInt(2, taskResultStatistics.getFailedCount());
+ preparedStatement.setTimestamp(3, new Timestamp(taskResultStatistics.getStatisticsTime().getTime()));
+ preparedStatement.setTimestamp(4, new Timestamp(taskResultStatistics.getCreationTime().getTime()));
+ preparedStatement.execute();
+ result = true;
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Insert taskResultStatistics to DB error:", ex);
+ }
+ return result;
+ }
+
+ /**
+ * 添加运行中的任务统计数据.
+ *
+ * @param taskRunningStatistics 运行中的任务统计数据对象
+ * @return 添加操作是否成功
+ */
+ public boolean add(final TaskRunningStatistics taskRunningStatistics) {
+ boolean result = false;
+ String sql = "INSERT INTO `" + TABLE_TASK_RUNNING_STATISTICS + "` (`running_count`, `statistics_time`, `creation_time`) VALUES (?, ?, ?);";
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
+ preparedStatement.setInt(1, taskRunningStatistics.getRunningCount());
+ preparedStatement.setTimestamp(2, new Timestamp(taskRunningStatistics.getStatisticsTime().getTime()));
+ preparedStatement.setTimestamp(3, new Timestamp(taskRunningStatistics.getCreationTime().getTime()));
+ preparedStatement.execute();
+ result = true;
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Insert taskRunningStatistics to DB error:", ex);
+ }
+ return result;
+ }
+
+ /**
+ * 添加运行中的作业统计数据.
+ *
+ * @param jobRunningStatistics 运行中的作业统计数据对象
+ * @return 添加操作是否成功
+ */
+ public boolean add(final JobRunningStatistics jobRunningStatistics) {
+ boolean result = false;
+ String sql = "INSERT INTO `" + TABLE_JOB_RUNNING_STATISTICS + "` (`running_count`, `statistics_time`, `creation_time`) VALUES (?, ?, ?);";
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
+ preparedStatement.setInt(1, jobRunningStatistics.getRunningCount());
+ preparedStatement.setTimestamp(2, new Timestamp(jobRunningStatistics.getStatisticsTime().getTime()));
+ preparedStatement.setTimestamp(3, new Timestamp(jobRunningStatistics.getCreationTime().getTime()));
+ preparedStatement.execute();
+ result = true;
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Insert jobRunningStatistics to DB error:", ex);
+ }
+ return result;
+ }
+
+ /**
+ * 添加作业注册统计数据.
+ *
+ * @param jobRegisterStatistics 作业注册统计数据对象
+ * @return 添加操作是否成功
+ */
+ public boolean add(final JobRegisterStatistics jobRegisterStatistics) {
+ boolean result = false;
+ String sql = "INSERT INTO `" + TABLE_JOB_REGISTER_STATISTICS + "` (`registered_count`, `statistics_time`, `creation_time`) VALUES (?, ?, ?);";
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
+ preparedStatement.setInt(1, jobRegisterStatistics.getRegisteredCount());
+ preparedStatement.setTimestamp(2, new Timestamp(jobRegisterStatistics.getStatisticsTime().getTime()));
+ preparedStatement.setTimestamp(3, new Timestamp(jobRegisterStatistics.getCreationTime().getTime()));
+ preparedStatement.execute();
+ result = true;
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Insert jobRegisterStatistics to DB error:", ex);
+ }
+ return result;
+ }
+
+ /**
+ * 获取任务运行结果统计数据集合.
+ *
+ * @param from 统计开始时间
+ * @param statisticInterval 统计时间间隔
+ * @return 任务运行结果统计数据集合
+ */
+ public List<TaskResultStatistics> findTaskResultStatistics(final Date from, final StatisticInterval statisticInterval) {
+ List<TaskResultStatistics> result = new LinkedList<>();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String sql = String.format("SELECT id, success_count, failed_count, statistics_time, creation_time FROM %s WHERE statistics_time >= '%s' order by id ASC",
+ TABLE_TASK_RESULT_STATISTICS + "_" + statisticInterval, formatter.format(from));
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ while (resultSet.next()) {
+ TaskResultStatistics taskResultStatistics = new TaskResultStatistics(resultSet.getLong(1), resultSet.getInt(2), resultSet.getInt(3),
+ statisticInterval, new Date(resultSet.getTimestamp(4).getTime()), new Date(resultSet.getTimestamp(5).getTime()));
+ result.add(taskResultStatistics);
+ }
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Fetch taskResultStatistics from DB error:", ex);
+ }
+ return result;
+ }
+
+ /**
+ * 获取合计后的任务运行结果统计数据.
+ *
+ * @param from 统计开始时间
+ * @param statisticInterval 统计时间间隔
+ * @return 合计后的任务运行结果统计数据对象
+ */
+ public TaskResultStatistics getSummedTaskResultStatistics(final Date from, final StatisticInterval statisticInterval) {
+ TaskResultStatistics result = new TaskResultStatistics(0, 0, statisticInterval, new Date());
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String sql = String.format("SELECT sum(success_count), sum(failed_count) FROM %s WHERE statistics_time >= '%s'",
+ TABLE_TASK_RESULT_STATISTICS + "_" + statisticInterval, formatter.format(from));
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ while (resultSet.next()) {
+ result = new TaskResultStatistics(resultSet.getInt(1), resultSet.getInt(2), statisticInterval, new Date());
+ }
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Fetch summed taskResultStatistics from DB error:", ex);
+ }
+ return result;
+ }
+
+ /**
+ * 获取最近一条任务运行结果统计数据.
+ *
+ * @param statisticInterval 统计时间间隔
+ * @return 任务运行结果统计数据对象
+ */
+ public Optional<TaskResultStatistics> findLatestTaskResultStatistics(final StatisticInterval statisticInterval) {
+ TaskResultStatistics result = null;
+ String sql = String.format("SELECT id, success_count, failed_count, statistics_time, creation_time FROM %s order by id DESC LIMIT 1",
+ TABLE_TASK_RESULT_STATISTICS + "_" + statisticInterval);
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ while (resultSet.next()) {
+ result = new TaskResultStatistics(resultSet.getLong(1), resultSet.getInt(2), resultSet.getInt(3),
+ statisticInterval, new Date(resultSet.getTimestamp(4).getTime()), new Date(resultSet.getTimestamp(5).getTime()));
+ }
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Fetch latest taskResultStatistics from DB error:", ex);
+ }
+ return Optional.fromNullable(result);
+ }
+
+ /**
+ * 获取运行中的任务统计数据集合.
+ *
+ * @param from 统计开始时间
+ * @return 运行中的任务统计数据集合
+ */
+ public List<TaskRunningStatistics> findTaskRunningStatistics(final Date from) {
+ List<TaskRunningStatistics> result = new LinkedList<>();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String sql = String.format("SELECT id, running_count, statistics_time, creation_time FROM %s WHERE statistics_time >= '%s' order by id ASC",
+ TABLE_TASK_RUNNING_STATISTICS, formatter.format(from));
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ while (resultSet.next()) {
+ TaskRunningStatistics taskRunningStatistics = new TaskRunningStatistics(resultSet.getLong(1), resultSet.getInt(2),
+ new Date(resultSet.getTimestamp(3).getTime()), new Date(resultSet.getTimestamp(4).getTime()));
+ result.add(taskRunningStatistics);
+ }
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Fetch taskRunningStatistics from DB error:", ex);
+ }
+ return result;
+ }
+
+ /**
+ * 获取运行中的任务统计数据集合.
+ *
+ * @param from 统计开始时间
+ * @return 运行中的任务统计数据集合
+ */
+ public List<JobRunningStatistics> findJobRunningStatistics(final Date from) {
+ List<JobRunningStatistics> result = new LinkedList<>();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String sql = String.format("SELECT id, running_count, statistics_time, creation_time FROM %s WHERE statistics_time >= '%s' order by id ASC",
+ TABLE_JOB_RUNNING_STATISTICS, formatter.format(from));
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ while (resultSet.next()) {
+ JobRunningStatistics jobRunningStatistics = new JobRunningStatistics(resultSet.getLong(1), resultSet.getInt(2),
+ new Date(resultSet.getTimestamp(3).getTime()), new Date(resultSet.getTimestamp(4).getTime()));
+ result.add(jobRunningStatistics);
+ }
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Fetch jobRunningStatistics from DB error:", ex);
+ }
+ return result;
+ }
+
+ /**
+ * 获取最近一条运行中的任务统计数据.
+ *
+ * @return 运行中的任务统计数据对象
+ */
+ public Optional<TaskRunningStatistics> findLatestTaskRunningStatistics() {
+ TaskRunningStatistics result = null;
+ String sql = String.format("SELECT id, running_count, statistics_time, creation_time FROM %s order by id DESC LIMIT 1",
+ TABLE_TASK_RUNNING_STATISTICS);
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ while (resultSet.next()) {
+ result = new TaskRunningStatistics(resultSet.getLong(1), resultSet.getInt(2),
+ new Date(resultSet.getTimestamp(3).getTime()), new Date(resultSet.getTimestamp(4).getTime()));
+ }
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Fetch latest taskRunningStatistics from DB error:", ex);
+ }
+ return Optional.fromNullable(result);
+ }
+
+ /**
+ * 获取最近一条运行中的任务统计数据.
+ *
+ * @return 运行中的任务统计数据对象
+ */
+ public Optional<JobRunningStatistics> findLatestJobRunningStatistics() {
+ JobRunningStatistics result = null;
+ String sql = String.format("SELECT id, running_count, statistics_time, creation_time FROM %s order by id DESC LIMIT 1",
+ TABLE_JOB_RUNNING_STATISTICS);
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ while (resultSet.next()) {
+ result = new JobRunningStatistics(resultSet.getLong(1), resultSet.getInt(2),
+ new Date(resultSet.getTimestamp(3).getTime()), new Date(resultSet.getTimestamp(4).getTime()));
+ }
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Fetch latest jobRunningStatistics from DB error:", ex);
+ }
+ return Optional.fromNullable(result);
+ }
+
+ /**
+ * 获取作业注册统计数据集合.
+ *
+ * @param from 统计开始时间
+ * @return 作业注册统计数据集合
+ */
+ public List<JobRegisterStatistics> findJobRegisterStatistics(final Date from) {
+ List<JobRegisterStatistics> result = new LinkedList<>();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String sql = String.format("SELECT id, registered_count, statistics_time, creation_time FROM %s WHERE statistics_time >= '%s' order by id ASC",
+ TABLE_JOB_REGISTER_STATISTICS, formatter.format(from));
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ while (resultSet.next()) {
+ JobRegisterStatistics jobRegisterStatistics = new JobRegisterStatistics(resultSet.getLong(1), resultSet.getInt(2),
+ new Date(resultSet.getTimestamp(3).getTime()), new Date(resultSet.getTimestamp(4).getTime()));
+ result.add(jobRegisterStatistics);
+ }
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Fetch jobRegisterStatistics from DB error:", ex);
+ }
+ return result;
+ }
+
+ /**
+ * 获取最近一条作业注册统计数据.
+ *
+ * @return 作业注册统计数据对象
+ */
+ public Optional<JobRegisterStatistics> findLatestJobRegisterStatistics() {
+ JobRegisterStatistics result = null;
+ String sql = String.format("SELECT id, registered_count, statistics_time, creation_time FROM %s order by id DESC LIMIT 1",
+ TABLE_JOB_REGISTER_STATISTICS);
+ try (
+ Connection conn = dataSource.getConnection();
+ PreparedStatement preparedStatement = conn.prepareStatement(sql);
+ ResultSet resultSet = preparedStatement.executeQuery()
+ ) {
+ while (resultSet.next()) {
+ result = new JobRegisterStatistics(resultSet.getLong(1), resultSet.getInt(2),
+ new Date(resultSet.getTimestamp(3).getTime()), new Date(resultSet.getTimestamp(4).getTime()));
+ }
+ } catch (final SQLException ex) {
+ // TODO 记录失败直接输出日志,未来可考虑配置化
+ log.error("Fetch latest jobRegisterStatistics from DB error:", ex);
+ }
+ return Optional.fromNullable(result);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/job/JobExecutionTypeStatistics.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/job/JobExecutionTypeStatistics.java
new file mode 100644
index 0000000..1cc18a3
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/job/JobExecutionTypeStatistics.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.statistics.type.job;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 作业执行类型统计数据.
+ *
+ * @author liguangyun
+ */
+@Getter
+@AllArgsConstructor
+public final class JobExecutionTypeStatistics {
+
+ private int transientJobCount;
+
+ private int daemonJobCount;
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/job/JobRegisterStatistics.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/job/JobRegisterStatistics.java
new file mode 100644
index 0000000..c47b445
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/job/JobRegisterStatistics.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.statistics.type.job;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Date;
+
+/**
+ * 作业注册到Cloud平台统计数据.
+ *
+ * @author liguangyun
+ */
+@Getter
+@RequiredArgsConstructor
+@AllArgsConstructor
+public final class JobRegisterStatistics {
+
+ private long id;
+
+ private final int registeredCount;
+
+ private final Date statisticsTime;
+
+ private Date creationTime = new Date();
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/job/JobRunningStatistics.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/job/JobRunningStatistics.java
new file mode 100644
index 0000000..fa77b5b
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/job/JobRunningStatistics.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.statistics.type.job;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Date;
+
+/**
+ * 运行中的作业统计数据.
+ *
+ * @author liguangyun
+ */
+@Getter
+@RequiredArgsConstructor
+@AllArgsConstructor
+public final class JobRunningStatistics {
+
+ private long id;
+
+ private final int runningCount;
+
+ private final Date statisticsTime;
+
+ private Date creationTime = new Date();
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/job/JobTypeStatistics.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/job/JobTypeStatistics.java
new file mode 100644
index 0000000..f9b5b0f
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/job/JobTypeStatistics.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.statistics.type.job;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 作业类型统计数据.
+ *
+ * @author liguangyun
+ */
+@Getter
+@AllArgsConstructor
+public final class JobTypeStatistics {
+
+ private int scriptJobCount;
+
+ private int simpleJobCount;
+
+ private int dataflowJobCount;
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/task/TaskResultStatistics.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/task/TaskResultStatistics.java
new file mode 100644
index 0000000..d151bb1
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/task/TaskResultStatistics.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.statistics.type.task;
+
+import io.elasticjob.cloud.statistics.StatisticInterval;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Date;
+
+/**
+ * 任务运行结果统计数据.
+ *
+ * @author liguangyun
+ */
+@Getter
+@AllArgsConstructor
+@RequiredArgsConstructor
+public final class TaskResultStatistics {
+
+ private long id;
+
+ private final int successCount;
+
+ private final int failedCount;
+
+ private final StatisticInterval statisticInterval;
+
+ private final Date statisticsTime;
+
+ private Date creationTime = new Date();
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/task/TaskRunningStatistics.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/task/TaskRunningStatistics.java
new file mode 100644
index 0000000..2c9de69
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/statistics/type/task/TaskRunningStatistics.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.statistics.type.task;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Date;
+
+/**
+ * 运行中的任务统计数据.
+ *
+ * @author liguangyun
+ */
+@Getter
+@RequiredArgsConstructor
+@AllArgsConstructor
+public final class TaskRunningStatistics {
+
+ private long id;
+
+ private final int runningCount;
+
+ private final Date statisticsTime;
+
+ private Date creationTime = new Date();
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/concurrent/BlockUtils.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/concurrent/BlockUtils.java
new file mode 100644
index 0000000..66096f4
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/concurrent/BlockUtils.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.concurrent;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class BlockUtils {
+
+ public static void waitingShortTime() {
+ sleep(100L);
+ }
+
+ public static void sleep(final long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (final InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/concurrent/ExecutorServiceObject.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/concurrent/ExecutorServiceObject.java
new file mode 100644
index 0000000..e8c2417
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/concurrent/ExecutorServiceObject.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.concurrent;
+
+import com.google.common.base.Joiner;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 线程池执行服务对象.
+ *
+ * @author zhangliang
+ */
+public final class ExecutorServiceObject {
+
+ private final ThreadPoolExecutor threadPoolExecutor;
+
+ private final BlockingQueue<Runnable> workQueue;
+
+ public ExecutorServiceObject(final String namingPattern, final int threadSize) {
+ workQueue = new LinkedBlockingQueue<>();
+ threadPoolExecutor = new ThreadPoolExecutor(threadSize, threadSize, 5L, TimeUnit.MINUTES, workQueue,
+ new BasicThreadFactory.Builder().namingPattern(Joiner.on("-").join(namingPattern, "%s")).build());
+ threadPoolExecutor.allowCoreThreadTimeOut(true);
+ }
+
+ /**
+ * 创建线程池服务对象.
+ *
+ * @return 线程池服务对象
+ */
+ public ExecutorService createExecutorService() {
+ return MoreExecutors.listeningDecorator(MoreExecutors.getExitingExecutorService(threadPoolExecutor));
+ }
+
+ public boolean isShutdown() {
+ return threadPoolExecutor.isShutdown();
+ }
+
+ /**
+ * 获取当前活跃的线程数.
+ *
+ * @return 当前活跃的线程数
+ */
+ public int getActiveThreadCount() {
+ return threadPoolExecutor.getActiveCount();
+ }
+
+ /**
+ * 获取待执行任务数量.
+ *
+ * @return 待执行任务数量
+ */
+ public int getWorkQueueSize() {
+ return workQueue.size();
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/config/ShardingItemParameters.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/config/ShardingItemParameters.java
new file mode 100644
index 0000000..7fef4a5
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/config/ShardingItemParameters.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.config;
+
+import com.google.common.base.Strings;
+import io.elasticjob.cloud.exception.JobConfigurationException;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 分片序列号个性化参数.
+ *
+ * @author zhangliang
+ */
+@Getter
+public final class ShardingItemParameters {
+
+ private static final String PARAMETER_DELIMITER = ",";
+
+ private static final String KEY_VALUE_DELIMITER = "=";
+
+ private final Map<Integer, String> map;
+
+ public ShardingItemParameters(final String shardingItemParameters) {
+ map = toMap(shardingItemParameters);
+ }
+
+ private Map<Integer, String> toMap(final String originalShardingItemParameters) {
+ if (Strings.isNullOrEmpty(originalShardingItemParameters)) {
+ return Collections.emptyMap();
+ }
+ String[] shardingItemParameters = originalShardingItemParameters.split(PARAMETER_DELIMITER);
+ Map<Integer, String> result = new HashMap<>(shardingItemParameters.length);
+ for (String each : shardingItemParameters) {
+ ShardingItem shardingItem = parse(each, originalShardingItemParameters);
+ result.put(shardingItem.item, shardingItem.parameter);
+ }
+ return result;
+ }
+
+ private ShardingItem parse(final String shardingItemParameter, final String originalShardingItemParameters) {
+ String[] pair = shardingItemParameter.trim().split(KEY_VALUE_DELIMITER);
+ if (2 != pair.length) {
+ throw new JobConfigurationException("Sharding item parameters '%s' format error, should be int=xx,int=xx", originalShardingItemParameters);
+ }
+ try {
+ return new ShardingItem(Integer.parseInt(pair[0].trim()), pair[1].trim());
+ } catch (final NumberFormatException ex) {
+ throw new JobConfigurationException("Sharding item parameters key '%s' is not an integer.", pair[0]);
+ }
+ }
+
+ /**
+ * 分片项.
+ */
+ @AllArgsConstructor
+ private static final class ShardingItem {
+
+ private final int item;
+
+ private final String parameter;
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/config/ShardingItems.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/config/ShardingItems.java
new file mode 100644
index 0000000..86bbe4d
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/config/ShardingItems.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.config;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import lombok.Getter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 分片项工具类.
+ *
+ * @author zhangliang
+ */
+@Getter
+public final class ShardingItems {
+
+ private static final String DELIMITER = ",";
+
+ /**
+ * 根据分片项字符串获取分片项列表.
+ *
+ * @param itemsString 分片项字符串
+ * @return 分片项列表
+ */
+ public static List<Integer> toItemList(final String itemsString) {
+ if (Strings.isNullOrEmpty(itemsString)) {
+ return Collections.emptyList();
+ }
+ String[] items = itemsString.split(DELIMITER);
+ List<Integer> result = new ArrayList<>(items.length);
+ for (String each : items) {
+ int item = Integer.parseInt(each);
+ if (!result.contains(item)) {
+ result.add(item);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * 根据分片项列表获取分片项字符串.
+ *
+ * @param items 分片项列表
+ * @return 分片项字符串
+ */
+ public static String toItemsString(final List<Integer> items) {
+ return items.isEmpty() ? "" : Joiner.on(DELIMITER).join(items);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/digest/Encryption.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/digest/Encryption.java
new file mode 100644
index 0000000..4701518
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/digest/Encryption.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.digest;
+
+import io.elasticjob.cloud.exception.JobSystemException;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * 字符串加密工具类.
+ *
+ * @author zhangliang
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class Encryption {
+
+ private static final String MD5 = "MD5";
+
+ /**
+ * 采用MD5算法加密字符串.
+ *
+ * @param str 需要加密的字符串
+ * @return 加密后的字符串
+ */
+ public static String md5(final String str) {
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance(MD5);
+ messageDigest.update(str.getBytes());
+ return new BigInteger(1, messageDigest.digest()).toString(16);
+ } catch (final NoSuchAlgorithmException ex) {
+ throw new JobSystemException(ex);
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/env/HostException.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/env/HostException.java
new file mode 100644
index 0000000..48168c9
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/env/HostException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.env;
+
+import java.io.IOException;
+
+/**
+ * 网络主机异常.
+ *
+ * @author zhangliang
+ */
+public final class HostException extends RuntimeException {
+
+ private static final long serialVersionUID = 3589264847881174997L;
+
+ public HostException(final IOException cause) {
+ super(cause);
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/env/IpUtils.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/env/IpUtils.java
new file mode 100644
index 0000000..3552182
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/env/IpUtils.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.env;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Enumeration;
+
+/**
+ * 获取真实本机网络的服务.
+ *
+ * @author zhangliang
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class IpUtils {
+
+ /**
+ * IP地址的正则表达式.
+ */
+ public static final String IP_REGEX = "((\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3})";
+
+ private static volatile String cachedIpAddress;
+
+ /**
+ * 获取本机IP地址.
+ *
+ * <p>
+ * 有限获取外网IP地址.
+ * 也有可能是链接着路由器的最终IP地址.
+ * </p>
+ *
+ * @return 本机IP地址
+ */
+ public static String getIp() {
+ if (null != cachedIpAddress) {
+ return cachedIpAddress;
+ }
+ Enumeration<NetworkInterface> netInterfaces;
+ try {
+ netInterfaces = NetworkInterface.getNetworkInterfaces();
+ } catch (final SocketException ex) {
+ throw new HostException(ex);
+ }
+ String localIpAddress = null;
+ while (netInterfaces.hasMoreElements()) {
+ NetworkInterface netInterface = netInterfaces.nextElement();
+ Enumeration<InetAddress> ipAddresses = netInterface.getInetAddresses();
+ while (ipAddresses.hasMoreElements()) {
+ InetAddress ipAddress = ipAddresses.nextElement();
+ if (isPublicIpAddress(ipAddress)) {
+ String publicIpAddress = ipAddress.getHostAddress();
+ cachedIpAddress = publicIpAddress;
+ return publicIpAddress;
+ }
+ if (isLocalIpAddress(ipAddress)) {
+ localIpAddress = ipAddress.getHostAddress();
+ }
+ }
+ }
+ cachedIpAddress = localIpAddress;
+ return localIpAddress;
+ }
+
+ private static boolean isPublicIpAddress(final InetAddress ipAddress) {
+ return !ipAddress.isSiteLocalAddress() && !ipAddress.isLoopbackAddress() && !isV6IpAddress(ipAddress);
+ }
+
+ private static boolean isLocalIpAddress(final InetAddress ipAddress) {
+ return ipAddress.isSiteLocalAddress() && !ipAddress.isLoopbackAddress() && !isV6IpAddress(ipAddress);
+ }
+
+ private static boolean isV6IpAddress(final InetAddress ipAddress) {
+ return ipAddress.getHostAddress().contains(":");
+ }
+
+ /**
+ * 获取本机Host名称.
+ *
+ * @return 本机Host名称
+ */
+ public static String getHostName() {
+ try {
+ return InetAddress.getLocalHost().getHostName();
+ } catch (final UnknownHostException ex) {
+ return "unknown";
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/env/TimeService.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/env/TimeService.java
new file mode 100644
index 0000000..d3aebaa
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/env/TimeService.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.env;
+
+/**
+ * 获取时间的服务.
+ *
+ * @author zhangliang
+ */
+public final class TimeService {
+
+ /**
+ * 获取当前时间的毫秒数.
+ *
+ * @return 当前时间的毫秒数
+ */
+ public long getCurrentMillis() {
+ return System.currentTimeMillis();
+ }
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/json/AbstractJobConfigurationGsonTypeAdapter.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/json/AbstractJobConfigurationGsonTypeAdapter.java
new file mode 100644
index 0000000..520c71f
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/json/AbstractJobConfigurationGsonTypeAdapter.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.json;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.elasticjob.cloud.api.JobType;
+import io.elasticjob.cloud.config.JobTypeConfiguration;
+import io.elasticjob.cloud.config.script.ScriptJobConfiguration;
+import io.elasticjob.cloud.config.JobCoreConfiguration;
+import io.elasticjob.cloud.config.JobRootConfiguration;
+import io.elasticjob.cloud.config.dataflow.DataflowJobConfiguration;
+import io.elasticjob.cloud.config.simple.SimpleJobConfiguration;
+import io.elasticjob.cloud.executor.handler.JobProperties;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 作业配置的Json转换适配器.
+ *
+ * @param <T> 作业配置对象泛型
+ *
+ * @author zhangliang
+ * @author caohao
+ */
+public abstract class AbstractJobConfigurationGsonTypeAdapter<T extends JobRootConfiguration> extends TypeAdapter<T> {
+
+ @Override
+ public T read(final JsonReader in) throws IOException {
+ String jobName = "";
+ String cron = "";
+ int shardingTotalCount = 0;
+ String shardingItemParameters = "";
+ String jobParameter = "";
+ boolean failover = false;
+ boolean misfire = failover;
+ String description = "";
+ JobProperties jobProperties = new JobProperties();
+ JobType jobType = null;
+ String jobClass = "";
+ boolean streamingProcess = false;
+ String scriptCommandLine = "";
+ Map<String, Object> customizedValueMap = new HashMap<>(32, 1);
+ in.beginObject();
+ while (in.hasNext()) {
+ String jsonName = in.nextName();
+ switch (jsonName) {
+ case "jobName":
+ jobName = in.nextString();
+ break;
+ case "cron":
+ cron = in.nextString();
+ break;
+ case "shardingTotalCount":
+ shardingTotalCount = in.nextInt();
+ break;
+ case "shardingItemParameters":
+ shardingItemParameters = in.nextString();
+ break;
+ case "jobParameter":
+ jobParameter = in.nextString();
+ break;
+ case "failover":
+ failover = in.nextBoolean();
+ break;
+ case "misfire":
+ misfire = in.nextBoolean();
+ break;
+ case "description":
+ description = in.nextString();
+ break;
+ case "jobProperties":
+ jobProperties = getJobProperties(in);
+ break;
+ case "jobType":
+ jobType = JobType.valueOf(in.nextString());
+ break;
+ case "jobClass":
+ jobClass = in.nextString();
+ break;
+ case "streamingProcess":
+ streamingProcess = in.nextBoolean();
+ break;
+ case "scriptCommandLine":
+ scriptCommandLine = in.nextString();
+ break;
+ default:
+ addToCustomizedValueMap(jsonName, in, customizedValueMap);
+ break;
+ }
+ }
+ in.endObject();
+ JobCoreConfiguration coreConfig = getJobCoreConfiguration(jobName, cron, shardingTotalCount, shardingItemParameters,
+ jobParameter, failover, misfire, description, jobProperties);
+ JobTypeConfiguration typeConfig = getJobTypeConfiguration(coreConfig, jobType, jobClass, streamingProcess, scriptCommandLine);
+ return getJobRootConfiguration(typeConfig, customizedValueMap);
+ }
+
+ private JobProperties getJobProperties(final JsonReader in) throws IOException {
+ JobProperties result = new JobProperties();
+ in.beginObject();
+ while (in.hasNext()) {
+ switch (in.nextName()) {
+ case "job_exception_handler":
+ result.put(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER.getKey(), in.nextString());
+ break;
+ case "executor_service_handler":
+ result.put(JobProperties.JobPropertiesEnum.EXECUTOR_SERVICE_HANDLER.getKey(), in.nextString());
+ break;
+ default:
+ break;
+ }
+ }
+ in.endObject();
+ return result;
+ }
+
+ protected abstract void addToCustomizedValueMap(final String jsonName, final JsonReader in, final Map<String, Object> customizedValueMap) throws IOException;
+
+ private JobCoreConfiguration getJobCoreConfiguration(final String jobName, final String cron, final int shardingTotalCount,
+ final String shardingItemParameters, final String jobParameter, final boolean failover,
+ final boolean misfire, final String description,
+ final JobProperties jobProperties) {
+ return JobCoreConfiguration.newBuilder(jobName, cron, shardingTotalCount)
+ .shardingItemParameters(shardingItemParameters).jobParameter(jobParameter).failover(failover).misfire(misfire).description(description)
+ .jobProperties(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER.getKey(), jobProperties.get(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER))
+ .jobProperties(JobProperties.JobPropertiesEnum.EXECUTOR_SERVICE_HANDLER.getKey(), jobProperties.get(JobProperties.JobPropertiesEnum.EXECUTOR_SERVICE_HANDLER))
+ .build();
+ }
+
+ private JobTypeConfiguration getJobTypeConfiguration(
+ final JobCoreConfiguration coreConfig, final JobType jobType, final String jobClass, final boolean streamingProcess, final String scriptCommandLine) {
+ Preconditions.checkNotNull(jobType, "jobType cannot be null.");
+ switch (jobType) {
+ case SIMPLE:
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(jobClass), "jobClass cannot be empty.");
+ return new SimpleJobConfiguration(coreConfig, jobClass);
+ case DATAFLOW:
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(jobClass), "jobClass cannot be empty.");
+ return new DataflowJobConfiguration(coreConfig, jobClass, streamingProcess);
+ case SCRIPT:
+ return new ScriptJobConfiguration(coreConfig, scriptCommandLine);
+ default:
+ throw new UnsupportedOperationException(String.valueOf(jobType));
+ }
+ }
+
+ protected abstract T getJobRootConfiguration(final JobTypeConfiguration typeConfig, final Map<String, Object> customizedValueMap);
+
+ @Override
+ public void write(final JsonWriter out, final T value) throws IOException {
+ out.beginObject();
+ out.name("jobName").value(value.getTypeConfig().getCoreConfig().getJobName());
+ out.name("jobClass").value(value.getTypeConfig().getJobClass());
+ out.name("jobType").value(value.getTypeConfig().getJobType().name());
+ out.name("cron").value(value.getTypeConfig().getCoreConfig().getCron());
+ out.name("shardingTotalCount").value(value.getTypeConfig().getCoreConfig().getShardingTotalCount());
+ out.name("shardingItemParameters").value(value.getTypeConfig().getCoreConfig().getShardingItemParameters());
+ out.name("jobParameter").value(value.getTypeConfig().getCoreConfig().getJobParameter());
+ out.name("failover").value(value.getTypeConfig().getCoreConfig().isFailover());
+ out.name("misfire").value(value.getTypeConfig().getCoreConfig().isMisfire());
+ out.name("description").value(value.getTypeConfig().getCoreConfig().getDescription());
+ out.name("jobProperties").jsonValue(value.getTypeConfig().getCoreConfig().getJobProperties().json());
+ if (value.getTypeConfig().getJobType() == JobType.DATAFLOW) {
+ DataflowJobConfiguration dataflowJobConfig = (DataflowJobConfiguration) value.getTypeConfig();
+ out.name("streamingProcess").value(dataflowJobConfig.isStreamingProcess());
+ } else if (value.getTypeConfig().getJobType() == JobType.SCRIPT) {
+ ScriptJobConfiguration scriptJobConfig = (ScriptJobConfiguration) value.getTypeConfig();
+ out.name("scriptCommandLine").value(scriptJobConfig.getScriptCommandLine());
+ }
+ writeCustomized(out, value);
+ out.endObject();
+ }
+
+ protected abstract void writeCustomized(final JsonWriter out, final T value) throws IOException;
+}
diff --git a/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/json/GsonFactory.java b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/json/GsonFactory.java
new file mode 100644
index 0000000..ada7d36
--- /dev/null
+++ b/elastic-job-cloud-common/src/main/java/io/elasticjob/cloud/util/json/GsonFactory.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.json;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.lang.reflect.Type;
+
+/**
+ * Gson构建器.
+ *
+ * @author caohao
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class GsonFactory {
+
+ private static final GsonBuilder GSON_BUILDER = new GsonBuilder();
+
+ private static volatile Gson gson = GSON_BUILDER.create();
+
+ /**
+ * 注册Gson解析对象.
+ *
+ * @param type Gson解析对象类型
+ * @param typeAdapter Gson解析对象适配器
+ */
+ public static synchronized void registerTypeAdapter(final Type type, final TypeAdapter typeAdapter) {
+ GSON_BUILDER.registerTypeAdapter(type, typeAdapter);
+ gson = GSON_BUILDER.create();
+ }
+
+ /**
+ * 获取Gson实例.
+ *
+ * @return Gson实例
+ */
+ public static Gson getGson() {
+ return gson;
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/AllCoreTests.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/AllCoreTests.java
new file mode 100644
index 0000000..5e03817
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/AllCoreTests.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud;
+
+import io.elasticjob.cloud.api.AllApiTests;
+import io.elasticjob.cloud.executor.AllExecutorTests;
+import io.elasticjob.cloud.statistics.AllStatisticsTests;
+import io.elasticjob.cloud.config.AllConfigTests;
+import io.elasticjob.cloud.context.AllContextTests;
+import io.elasticjob.cloud.event.AllEventTests;
+import io.elasticjob.cloud.exception.AllExceptionTests;
+import io.elasticjob.cloud.reg.AllRegTests;
+import io.elasticjob.cloud.util.AllUtilTests;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses({
+ AllRegTests.class,
+ AllContextTests.class,
+ AllApiTests.class,
+ AllConfigTests.class,
+ AllExecutorTests.class,
+ AllEventTests.class,
+ AllExceptionTests.class,
+ AllStatisticsTests.class,
+ AllUtilTests.class
+ })
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class AllCoreTests {
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/api/AllApiTests.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/api/AllApiTests.java
new file mode 100644
index 0000000..07ec020
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/api/AllApiTests.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.api;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses(ShardingContextTest.class)
+public final class AllApiTests {
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/api/ShardingContextTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/api/ShardingContextTest.java
new file mode 100644
index 0000000..ee47ab1
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/api/ShardingContextTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.api;
+
+import io.elasticjob.cloud.executor.ShardingContexts;
+import io.elasticjob.cloud.fixture.ShardingContextsBuilder;
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public final class ShardingContextTest {
+
+ @Test
+ public void assertNew() {
+ ShardingContexts shardingContexts = ShardingContextsBuilder.getMultipleShardingContexts();
+ ShardingContext actual = new ShardingContext(shardingContexts, 1);
+ assertThat(actual.getJobName(), is(shardingContexts.getJobName()));
+ assertThat(actual.getTaskId(), is(shardingContexts.getTaskId()));
+ assertThat(actual.getShardingTotalCount(), is(shardingContexts.getShardingTotalCount()));
+ assertThat(actual.getJobParameter(), is(shardingContexts.getJobParameter()));
+ assertThat(actual.getShardingItem(), is(1));
+ assertThat(actual.getShardingParameter(), is(shardingContexts.getShardingItemParameters().get(1)));
+ }
+
+ @Test
+ public void assertToString() {
+ assertThat(new ShardingContext(ShardingContextsBuilder.getMultipleShardingContexts(), 1).toString(),
+ is("ShardingContext(jobName=test_job, taskId=fake_task_id, shardingTotalCount=2, jobParameter=, shardingItem=1, shardingParameter=B)"));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/config/AllConfigTests.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/config/AllConfigTests.java
new file mode 100644
index 0000000..b49ce04
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/config/AllConfigTests.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.config;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses(JobCoreConfigurationTest.class)
+public final class AllConfigTests {
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/config/JobCoreConfigurationTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/config/JobCoreConfigurationTest.java
new file mode 100644
index 0000000..744f54a
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/config/JobCoreConfigurationTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.config;
+
+import io.elasticjob.cloud.executor.handler.impl.DefaultJobExceptionHandler;
+import io.elasticjob.cloud.fixture.handler.IgnoreJobExceptionHandler;
+import io.elasticjob.cloud.executor.handler.JobProperties;
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public final class JobCoreConfigurationTest {
+
+ @Test
+ public void assertBuildAllProperties() {
+ JobCoreConfiguration actual = JobCoreConfiguration.newBuilder("test_job", "0/1 * * * * ?", 3)
+ .shardingItemParameters("0=a,1=b,2=c").jobParameter("param").failover(true).misfire(false).description("desc")
+ .jobProperties("job_exception_handler", IgnoreJobExceptionHandler.class.getName()).build();
+ assertRequiredProperties(actual);
+ assertThat(actual.getShardingItemParameters(), is("0=a,1=b,2=c"));
+ assertThat(actual.getJobParameter(), is("param"));
+ assertTrue(actual.isFailover());
+ assertFalse(actual.isMisfire());
+ assertThat(actual.getDescription(), is("desc"));
+ assertThat(actual.getJobProperties().get(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER), is(IgnoreJobExceptionHandler.class.getName()));
+ }
+
+ @Test
+ public void assertBuildRequiredProperties() {
+ JobCoreConfiguration actual = JobCoreConfiguration.newBuilder("test_job", "0/1 * * * * ?", 3).build();
+ assertRequiredProperties(actual);
+ assertDefaultValues(actual);
+ }
+
+ @Test
+ public void assertBuildWhenOptionalParametersIsNull() {
+ //noinspection NullArgumentToVariableArgMethod
+ JobCoreConfiguration actual = JobCoreConfiguration.newBuilder("test_job", "0/1 * * * * ?", 3).shardingItemParameters(null).jobParameter(null).description(null).build();
+ assertRequiredProperties(actual);
+ assertDefaultValues(actual);
+ }
+
+ private void assertRequiredProperties(final JobCoreConfiguration actual) {
+ assertThat(actual.getJobName(), is("test_job"));
+ assertThat(actual.getCron(), is("0/1 * * * * ?"));
+ assertThat(actual.getShardingTotalCount(), is(3));
+ }
+
+ private void assertDefaultValues(final JobCoreConfiguration actual) {
+ assertThat(actual.getShardingItemParameters(), is(""));
+ assertThat(actual.getJobParameter(), is(""));
+ assertFalse(actual.isFailover());
+ assertTrue(actual.isMisfire());
+ assertThat(actual.getDescription(), is(""));
+ assertThat(actual.getJobProperties().get(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER), is(DefaultJobExceptionHandler.class.getName()));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void assertBuildWhenJobNameIsNull() {
+ JobCoreConfiguration.newBuilder(null, "0/1 * * * * ?", 3).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void assertBuildWhenCronIsNull() {
+ JobCoreConfiguration.newBuilder("test_job", null, 3).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void assertBuildWhenTotalSHardingCountIsNegative() {
+ JobCoreConfiguration.newBuilder(null, "0/1 * * * * ?", -1).build();
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/context/AllContextTests.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/context/AllContextTests.java
new file mode 100644
index 0000000..76f099f
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/context/AllContextTests.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.context;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses(TaskContextTest.class)
+public final class AllContextTests {
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/context/TaskContextTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/context/TaskContextTest.java
new file mode 100644
index 0000000..1b0dddc
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/context/TaskContextTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.context;
+
+import com.google.common.collect.Lists;
+import io.elasticjob.cloud.fixture.context.TaskNode;
+import org.hamcrest.core.Is;
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.StringStartsWith.startsWith;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public final class TaskContextTest {
+
+ @Test
+ public void assertNew() {
+ TaskContext actual = new TaskContext("test_job", Lists.newArrayList(0), ExecutionType.READY, "slave-S0");
+ assertThat(actual.getMetaInfo().getJobName(), is("test_job"));
+ assertThat(actual.getMetaInfo().getShardingItems().get(0), is(0));
+ assertThat(actual.getType(), is(ExecutionType.READY));
+ assertThat(actual.getSlaveId(), is("slave-S0"));
+ assertThat(actual.getId(), startsWith(TaskNode.builder().build().getTaskNodeValue().substring(0, TaskNode.builder().build().getTaskNodeValue().length() - 1)));
+ }
+
+ @Test
+ public void assertNewWithoutSlaveId() {
+ TaskContext actual = new TaskContext("test_job", Lists.newArrayList(0), ExecutionType.READY);
+ assertThat(actual.getSlaveId(), is("unassigned-slave"));
+ }
+
+ @Test
+ public void assertGetMetaInfo() {
+ TaskContext actual = new TaskContext("test_job", Lists.newArrayList(0), ExecutionType.READY, "slave-S0");
+ assertThat(actual.getMetaInfo().toString(), is("test_job@-@0"));
+ }
+
+ @Test
+ public void assertTaskContextFrom() {
+ TaskContext actual = TaskContext.from(TaskNode.builder().build().getTaskNodeValue());
+ assertThat(actual.getId(), Is.is(TaskNode.builder().build().getTaskNodeValue()));
+ assertThat(actual.getMetaInfo().getJobName(), is("test_job"));
+ assertThat(actual.getMetaInfo().getShardingItems().get(0), is(0));
+ assertThat(actual.getType(), is(ExecutionType.READY));
+ assertThat(actual.getSlaveId(), is("slave-S0"));
+ }
+
+ @Test
+ public void assertMetaInfoFromWithMetaInfo() {
+ TaskContext.MetaInfo actual = TaskContext.MetaInfo.from("test_job@-@1");
+ assertThat(actual.getJobName(), is("test_job"));
+ assertThat(actual.getShardingItems().get(0), is(1));
+ }
+
+ @Test
+ public void assertMetaInfoFromWithTaskId() {
+ TaskContext.MetaInfo actual = TaskContext.MetaInfo.from("test_job@-@1@-@READY@-@unassigned-slave@-@0");
+ assertThat(actual.getJobName(), is("test_job"));
+ assertThat(actual.getShardingItems().get(0), is(1));
+ }
+
+ @Test
+ public void assertMetaInfoFromWithMetaInfoWithoutShardingItems() {
+ TaskContext.MetaInfo actual = TaskContext.MetaInfo.from("test_job@-@");
+ assertThat(actual.getJobName(), is("test_job"));
+ assertTrue(actual.getShardingItems().isEmpty());
+ }
+
+ @Test
+ public void assertMetaInfoFromWithTaskIdWithoutShardingItems() {
+ TaskContext.MetaInfo actual = TaskContext.MetaInfo.from("test_job@-@@-@READY@-@unassigned-slave@-@0");
+ assertThat(actual.getJobName(), is("test_job"));
+ assertTrue(actual.getShardingItems().isEmpty());
+ }
+
+ @Test
+ public void assertGetIdForUnassignedSlave() {
+ assertThat(TaskContext.getIdForUnassignedSlave("test_job@-@0@-@READY@-@slave-S0@-@0"), is("test_job@-@0@-@READY@-@unassigned-slave@-@0"));
+ }
+
+ @Test
+ public void assertGetTaskName() {
+ TaskContext actual = TaskContext.from(TaskNode.builder().build().getTaskNodeValue());
+ assertThat(actual.getTaskName(), is("test_job@-@0@-@READY@-@slave-S0"));
+ }
+
+ @Test
+ public void assertGetExecutorId() {
+ TaskContext actual = TaskContext.from(TaskNode.builder().build().getTaskNodeValue());
+ assertThat(actual.getExecutorId("app"), is("app@-@slave-S0"));
+ }
+
+ @Test
+ public void assertSetSlaveId() {
+ TaskContext actual = new TaskContext("test_job", Lists.newArrayList(0), ExecutionType.READY, "slave-S0");
+ assertThat(actual.getSlaveId(), is("slave-S0"));
+ actual.setSlaveId("slave-S1");
+ assertThat(actual.getSlaveId(), is("slave-S1"));
+ }
+
+ @Test
+ public void assertSetIdle() {
+ TaskContext actual = new TaskContext("test_job", Lists.newArrayList(0), ExecutionType.READY, "slave-S0");
+ assertFalse(actual.isIdle());
+ actual.setIdle(true);
+ assertTrue(actual.isIdle());
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/AllEventTests.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/AllEventTests.java
new file mode 100644
index 0000000..79fe411
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/AllEventTests.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event;
+
+import io.elasticjob.cloud.event.rdb.JobEventRdbConfigurationTest;
+import io.elasticjob.cloud.event.rdb.JobEventRdbIdentityTest;
+import io.elasticjob.cloud.event.rdb.JobEventRdbListenerTest;
+import io.elasticjob.cloud.event.rdb.JobEventRdbSearchTest;
+import io.elasticjob.cloud.event.rdb.JobEventRdbStorageTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ JobExecutionEventTest.class,
+ JobEventBusTest.class,
+ JobEventRdbIdentityTest.class,
+ JobEventRdbConfigurationTest.class,
+ JobEventRdbListenerTest.class,
+ JobEventRdbStorageTest.class,
+ JobEventRdbSearchTest.class
+ })
+public final class AllEventTests {
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/JobEventBusTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/JobEventBusTest.java
new file mode 100644
index 0000000..8bc11cc
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/JobEventBusTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event;
+
+import com.google.common.eventbus.EventBus;
+import io.elasticjob.cloud.event.fixture.JobEventCaller;
+import io.elasticjob.cloud.event.fixture.TestJobEventConfiguration;
+import io.elasticjob.cloud.event.fixture.TestJobEventListener;
+import io.elasticjob.cloud.event.type.JobExecutionEvent;
+import io.elasticjob.cloud.event.fixture.TestJobEventFailureConfiguration;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.unitils.util.ReflectionUtils;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class JobEventBusTest {
+
+ @Mock
+ private JobEventCaller jobEventCaller;
+
+ @Mock
+ private EventBus eventBus;
+
+ private JobEventBus jobEventBus;
+
+ @Test
+ public void assertRegisterFailure() throws NoSuchFieldException {
+ jobEventBus = new JobEventBus(new TestJobEventFailureConfiguration());
+ assertIsRegistered(false);
+ }
+
+ @Test
+ public void assertPost() throws InterruptedException, NoSuchFieldException {
+ jobEventBus = new JobEventBus(new TestJobEventConfiguration(jobEventCaller));
+ assertIsRegistered(true);
+ jobEventBus.post(new JobExecutionEvent("fake_task_id", "test_event_bus_job", JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0));
+ while (!TestJobEventListener.isExecutionEventCalled()) {
+ Thread.sleep(100L);
+ }
+ verify(jobEventCaller).call();
+ }
+
+ @Test
+ public void assertPostWithoutListener() throws NoSuchFieldException {
+ jobEventBus = new JobEventBus();
+ assertIsRegistered(false);
+ ReflectionUtils.setFieldValue(jobEventBus, "eventBus", eventBus);
+ jobEventBus.post(new JobExecutionEvent("fake_task_id", "test_event_bus_job", JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0));
+ verify(eventBus, times(0)).post(ArgumentMatchers.<JobEvent>any());
+ }
+
+ private void assertIsRegistered(final boolean actual) throws NoSuchFieldException {
+ assertThat((boolean) ReflectionUtils.getFieldValue(jobEventBus, JobEventBus.class.getDeclaredField("isRegistered")), is(actual));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/JobExecutionEventTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/JobExecutionEventTest.java
new file mode 100644
index 0000000..e234d3d
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/JobExecutionEventTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event;
+
+import io.elasticjob.cloud.event.type.JobExecutionEvent;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public final class JobExecutionEventTest {
+
+ @Test
+ public void assertNewJobExecutionEvent() {
+ JobExecutionEvent actual = new JobExecutionEvent("fake_task_id", "test_job", JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0);
+ assertThat(actual.getJobName(), is("test_job"));
+ assertThat(actual.getSource(), is(JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER));
+ assertThat(actual.getShardingItem(), is(0));
+ assertNotNull(actual.getHostname());
+ assertNotNull(actual.getStartTime());
+ assertNull(actual.getCompleteTime());
+ assertFalse(actual.isSuccess());
+ assertThat(actual.getFailureCause(), is(""));
+ }
+
+ @Test
+ public void assertExecutionSuccess() {
+ JobExecutionEvent startEvent = new JobExecutionEvent("fake_task_id", "test_job", JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0);
+ JobExecutionEvent successEvent = startEvent.executionSuccess();
+ assertNotNull(successEvent.getCompleteTime());
+ assertTrue(successEvent.isSuccess());
+ }
+
+ @Test
+ public void assertExecutionFailure() {
+ JobExecutionEvent startEvent = new JobExecutionEvent("fake_task_id", "test_job", JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0);
+ JobExecutionEvent failureEvent = startEvent.executionFailure(new RuntimeException("failure"));
+ assertNotNull(failureEvent.getCompleteTime());
+ assertFalse(failureEvent.isSuccess());
+ assertThat(failureEvent.getFailureCause(), startsWith("java.lang.RuntimeException: failure"));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/JobEventCaller.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/JobEventCaller.java
new file mode 100644
index 0000000..8b846e6
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/JobEventCaller.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.fixture;
+
+public interface JobEventCaller {
+
+ void call();
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/TestJobEventConfiguration.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/TestJobEventConfiguration.java
new file mode 100644
index 0000000..3d485f4
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/TestJobEventConfiguration.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.fixture;
+
+import io.elasticjob.cloud.event.JobEventConfiguration;
+import io.elasticjob.cloud.event.JobEventListener;
+import io.elasticjob.cloud.event.JobEventListenerConfigurationException;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public final class TestJobEventConfiguration extends TestJobEventIdentity implements JobEventConfiguration {
+
+ private final JobEventCaller jobEventCaller;
+
+ @Override
+ public JobEventListener createJobEventListener() throws JobEventListenerConfigurationException {
+ return new TestJobEventListener(jobEventCaller);
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/TestJobEventFailureConfiguration.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/TestJobEventFailureConfiguration.java
new file mode 100644
index 0000000..5024124
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/TestJobEventFailureConfiguration.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.fixture;
+
+import io.elasticjob.cloud.event.JobEventConfiguration;
+import io.elasticjob.cloud.event.JobEventListener;
+import io.elasticjob.cloud.event.JobEventListenerConfigurationException;
+
+public final class TestJobEventFailureConfiguration extends TestJobEventIdentity implements JobEventConfiguration {
+
+ @Override
+ public JobEventListener createJobEventListener() throws JobEventListenerConfigurationException {
+ throw new JobEventListenerConfigurationException(new RuntimeException("assert failure"));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/TestJobEventIdentity.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/TestJobEventIdentity.java
new file mode 100644
index 0000000..15738e3
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/TestJobEventIdentity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.fixture;
+
+import io.elasticjob.cloud.event.JobEventIdentity;
+
+public class TestJobEventIdentity implements JobEventIdentity {
+
+ @Override
+ public String getIdentity() {
+ return "test";
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/TestJobEventListener.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/TestJobEventListener.java
new file mode 100644
index 0000000..a7dc317
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/fixture/TestJobEventListener.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.fixture;
+
+import io.elasticjob.cloud.event.JobEventListener;
+import io.elasticjob.cloud.event.type.JobExecutionEvent;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public final class TestJobEventListener extends TestJobEventIdentity implements JobEventListener {
+
+ @Getter
+ private static volatile boolean executionEventCalled;
+
+ private final JobEventCaller jobEventCaller;
+
+ @Override
+ public void listen(final JobExecutionEvent jobExecutionEvent) {
+ jobEventCaller.call();
+ executionEventCalled = true;
+ }
+
+ @Override
+ public void listen(final JobStatusTraceEvent jobStatusTraceEvent) {
+ jobEventCaller.call();
+ }
+
+ public static void reset() {
+ executionEventCalled = false;
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbConfigurationTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbConfigurationTest.java
new file mode 100644
index 0000000..1d04ad9
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbConfigurationTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.rdb;
+
+import io.elasticjob.cloud.event.JobEventListenerConfigurationException;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public final class JobEventRdbConfigurationTest {
+
+ @Test
+ public void assertGetDataSource() throws JobEventListenerConfigurationException {
+ BasicDataSource dataSource = new BasicDataSource();
+ dataSource.setDriverClassName(org.h2.Driver.class.getName());
+ dataSource.setUrl("jdbc:h2:mem:job_event_storage");
+ dataSource.setUsername("sa");
+ dataSource.setPassword("");
+ assertThat((BasicDataSource) (new JobEventRdbConfiguration(dataSource).getDataSource()), is(dataSource));
+ }
+
+ @Test
+ public void assertCreateJobEventListenerSuccess() throws JobEventListenerConfigurationException {
+ BasicDataSource dataSource = new BasicDataSource();
+ dataSource.setDriverClassName(org.h2.Driver.class.getName());
+ dataSource.setUrl("jdbc:h2:mem:job_event_storage");
+ dataSource.setUsername("sa");
+ dataSource.setPassword("");
+ assertThat(new JobEventRdbConfiguration(dataSource).createJobEventListener(), instanceOf(JobEventRdbListener.class));
+ }
+
+ @Test(expected = JobEventListenerConfigurationException.class)
+ public void assertCreateJobEventListenerFailure() throws JobEventListenerConfigurationException {
+ new JobEventRdbConfiguration(new BasicDataSource()).createJobEventListener();
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbIdentityTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbIdentityTest.java
new file mode 100644
index 0000000..76b8812
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbIdentityTest.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.rdb;
+
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public final class JobEventRdbIdentityTest {
+
+ @Test
+ public void assertGetIdentity() {
+ assertThat(new JobEventRdbIdentity().getIdentity(), is("rdb"));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbListenerTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbListenerTest.java
new file mode 100644
index 0000000..d7d066d
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbListenerTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.rdb;
+
+import io.elasticjob.cloud.context.ExecutionType;
+import io.elasticjob.cloud.event.JobEventBus;
+import io.elasticjob.cloud.event.JobEventListenerConfigurationException;
+import io.elasticjob.cloud.event.type.JobExecutionEvent;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.unitils.util.ReflectionUtils;
+
+import java.sql.SQLException;
+
+import static org.mockito.Mockito.atMost;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class JobEventRdbListenerTest {
+
+ private static final String JOB_NAME = "test_rdb_event_listener";
+
+ @Mock
+ private JobEventRdbConfiguration jobEventRdbConfiguration;
+
+ @Mock
+ private JobEventRdbStorage repository;
+
+ private JobEventBus jobEventBus;
+
+ @Before
+ public void setUp() throws JobEventListenerConfigurationException, SQLException, NoSuchFieldException {
+ BasicDataSource dataSource = new BasicDataSource();
+ dataSource.setDriverClassName(org.h2.Driver.class.getName());
+ dataSource.setUrl("jdbc:h2:mem:job_event_storage");
+ dataSource.setUsername("sa");
+ dataSource.setPassword("");
+ JobEventRdbListener jobEventRdbListener = new JobEventRdbListener(dataSource);
+ ReflectionUtils.setFieldValue(jobEventRdbListener, "repository", repository);
+ when(jobEventRdbConfiguration.createJobEventListener()).thenReturn(jobEventRdbListener);
+ jobEventBus = new JobEventBus(jobEventRdbConfiguration);
+ }
+
+ @Test
+ public void assertPostJobExecutionEvent() {
+ JobExecutionEvent jobExecutionEvent = new JobExecutionEvent("fake_task_id", JOB_NAME, JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0);
+ jobEventBus.post(jobExecutionEvent);
+ verify(repository, atMost(1)).addJobExecutionEvent(jobExecutionEvent);
+ }
+
+ @Test
+ public void assertPostJobStatusTraceEvent() {
+ JobStatusTraceEvent jobStatusTraceEvent = new JobStatusTraceEvent(
+ JOB_NAME, "fake_task_id", "fake_slave_id", JobStatusTraceEvent.Source.LITE_EXECUTOR, ExecutionType.READY, "0", JobStatusTraceEvent.State.TASK_RUNNING, "message is empty.");
+ jobEventBus.post(jobStatusTraceEvent);
+ verify(repository, atMost(1)).addJobStatusTraceEvent(jobStatusTraceEvent);
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbSearchTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbSearchTest.java
new file mode 100644
index 0000000..bab6943
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbSearchTest.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.rdb;
+
+import io.elasticjob.cloud.context.ExecutionType;
+import io.elasticjob.cloud.event.type.JobExecutionEvent;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+public class JobEventRdbSearchTest {
+
+ private static JobEventRdbStorage storage;
+
+ private static JobEventRdbSearch repository;
+
+ @BeforeClass
+ public static void setUpClass() throws SQLException {
+ BasicDataSource dataSource = new BasicDataSource();
+ dataSource.setDriverClassName(org.h2.Driver.class.getName());
+ dataSource.setUrl("jdbc:h2:mem:");
+ dataSource.setUsername("sa");
+ dataSource.setPassword("");
+ storage = new JobEventRdbStorage(dataSource);
+ repository = new JobEventRdbSearch(dataSource);
+ initStorage();
+ }
+
+ private static void initStorage() {
+ for (int i = 1; i <= 500; i++) {
+ JobExecutionEvent startEvent = new JobExecutionEvent("fake_task_id", "test_job_" + i, JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0);
+ storage.addJobExecutionEvent(startEvent);
+ if (i % 2 == 0) {
+ JobExecutionEvent successEvent = startEvent.executionSuccess();
+ storage.addJobExecutionEvent(successEvent);
+ }
+ storage.addJobStatusTraceEvent(
+ new JobStatusTraceEvent("test_job_" + i, "fake_failed_failover_task_id", "fake_slave_id",
+ JobStatusTraceEvent.Source.LITE_EXECUTOR, ExecutionType.FAILOVER, "0", JobStatusTraceEvent.State.TASK_FAILED, "message is empty."));
+ }
+ }
+
+ @Test
+ public void assertFindJobExecutionEventsWithPageSizeAndNumber() {
+ JobEventRdbSearch.Result<JobExecutionEvent> result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, null, null, null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(50, 1, null, null, null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(50));
+ result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(100, 5, null, null, null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(100));
+ result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(100, 6, null, null, null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(0));
+ }
+
+ @Test
+ public void assertFindJobExecutionEventsWithErrorPageSizeAndNumber() {
+ JobEventRdbSearch.Result<JobExecutionEvent> result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(-1, -1, null, null, null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ }
+
+ @Test
+ public void assertFindJobExecutionEventsWithSort() {
+ JobEventRdbSearch.Result<JobExecutionEvent> result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, "jobName", "ASC", null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ assertThat(result.getRows().get(0).getJobName(), is("test_job_1"));
+ result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, "jobName", "DESC", null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ assertThat(result.getRows().get(0).getJobName(), is("test_job_99"));
+ }
+
+ @Test
+ public void assertFindJobExecutionEventsWithErrorSort() {
+ JobEventRdbSearch.Result<JobExecutionEvent> result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, "jobName", "ERROR_SORT", null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ assertThat(result.getRows().get(0).getJobName(), is("test_job_1"));
+ result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, "notExistField", "ASC", null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ }
+
+ @Test
+ public void assertFindJobExecutionEventsWithTime() {
+ Date now = new Date();
+ Date tenMinutesBefore = new Date(now.getTime() - 10 * 60 * 1000);
+ JobEventRdbSearch.Result<JobExecutionEvent> result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, null, null, tenMinutesBefore, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, null, null, now, null, null));
+ assertThat(result.getTotal(), is(0));
+ assertThat(result.getRows().size(), is(0));
+ result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, null, null, null, tenMinutesBefore, null));
+ assertThat(result.getTotal(), is(0));
+ assertThat(result.getRows().size(), is(0));
+ result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, null, null, null, now, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, null, null, tenMinutesBefore, now, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ }
+
+ @Test
+ public void assertFindJobExecutionEventsWithFields() {
+ Map<String, Object> fields = new HashMap<>();
+ fields.put("isSuccess", "1");
+ JobEventRdbSearch.Result<JobExecutionEvent> result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, null, null, null, null, fields));
+ assertThat(result.getTotal(), is(250));
+ assertThat(result.getRows().size(), is(10));
+ fields.put("isSuccess", null);
+ fields.put("jobName", "test_job_1");
+ result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, null, null, null, null, fields));
+ assertThat(result.getTotal(), is(1));
+ assertThat(result.getRows().size(), is(1));
+ }
+
+ @Test
+ public void assertFindJobExecutionEventsWithErrorFields() {
+ Map<String, Object> fields = new HashMap<>();
+ fields.put("notExistField", "some value");
+ JobEventRdbSearch.Result<JobExecutionEvent> result = repository.findJobExecutionEvents(new JobEventRdbSearch.Condition(10, 1, null, null, null, null, fields));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ }
+
+ @Test
+ public void assertFindJobStatusTraceEventsWithPageSizeAndNumber() {
+ JobEventRdbSearch.Result<JobStatusTraceEvent> result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(10, 1, null, null, null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(50, 1, null, null, null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(50));
+ result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(100, 5, null, null, null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(100));
+ result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(100, 6, null, null, null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(0));
+ }
+
+ @Test
+ public void assertFindJobStatusTraceEventsWithErrorPageSizeAndNumber() {
+ JobEventRdbSearch.Result<JobStatusTraceEvent> result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(-1, -1, null, null, null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ }
+
+ @Test
+ public void assertFindJobStatusTraceEventsWithSort() {
+ JobEventRdbSearch.Result<JobStatusTraceEvent> result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(10, 1, "jobName", "ASC", null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ assertThat(result.getRows().get(0).getJobName(), is("test_job_1"));
+ result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(10, 1, "jobName", "DESC", null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ assertThat(result.getRows().get(0).getJobName(), is("test_job_99"));
+ }
+
+ @Test
+ public void assertFindJobStatusTraceEventsWithErrorSort() {
+ JobEventRdbSearch.Result<JobStatusTraceEvent> result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(10, 1, "jobName", "ERROR_SORT", null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ assertThat(result.getRows().get(0).getJobName(), is("test_job_1"));
+ result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(10, 1, "notExistField", "ASC", null, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ }
+
+ @Test
+ public void assertFindJobStatusTraceEventsWithTime() {
+ Date now = new Date();
+ Date tenMinutesBefore = new Date(now.getTime() - 10 * 60 * 1000);
+ JobEventRdbSearch.Result<JobStatusTraceEvent> result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(10, 1, null, null, tenMinutesBefore, null, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(10, 1, null, null, now, null, null));
+ assertThat(result.getTotal(), is(0));
+ assertThat(result.getRows().size(), is(0));
+ result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(10, 1, null, null, null, tenMinutesBefore, null));
+ assertThat(result.getTotal(), is(0));
+ assertThat(result.getRows().size(), is(0));
+ result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(10, 1, null, null, null, now, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(10, 1, null, null, tenMinutesBefore, now, null));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ }
+
+ @Test
+ public void assertFindJobStatusTraceEventsWithFields() {
+ Map<String, Object> fields = new HashMap<>();
+ fields.put("jobName", "test_job_1");
+ JobEventRdbSearch.Result<JobStatusTraceEvent> result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(10, 1, null, null, null, null, fields));
+ assertThat(result.getTotal(), is(1));
+ assertThat(result.getRows().size(), is(1));
+ }
+
+ @Test
+ public void assertFindJobStatusTraceEventsWithErrorFields() {
+ Map<String, Object> fields = new HashMap<>();
+ fields.put("notExistField", "some value");
+ JobEventRdbSearch.Result<JobStatusTraceEvent> result = repository.findJobStatusTraceEvents(new JobEventRdbSearch.Condition(10, 1, null, null, null, null, fields));
+ assertThat(result.getTotal(), is(500));
+ assertThat(result.getRows().size(), is(10));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbStorageTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbStorageTest.java
new file mode 100644
index 0000000..41a7f5b
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/event/rdb/JobEventRdbStorageTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.event.rdb;
+
+import io.elasticjob.cloud.context.ExecutionType;
+import io.elasticjob.cloud.event.type.JobExecutionEvent;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.junit.Assert.assertThat;
+
+public class JobEventRdbStorageTest {
+
+ private JobEventRdbStorage storage;
+
+ @Before
+ public void setup() throws SQLException {
+ BasicDataSource dataSource = new BasicDataSource();
+ dataSource.setDriverClassName(org.h2.Driver.class.getName());
+ dataSource.setUrl("jdbc:h2:mem:job_event_storage");
+ dataSource.setUsername("sa");
+ dataSource.setPassword("");
+ storage = new JobEventRdbStorage(dataSource);
+ }
+
+ @Test
+ public void assertAddJobExecutionEvent() throws SQLException {
+ assertTrue(storage.addJobExecutionEvent(new JobExecutionEvent("fake_task_id", "test_job", JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0)));
+ }
+
+ @Test
+ public void assertAddJobStatusTraceEvent() throws SQLException {
+ assertTrue(storage.addJobStatusTraceEvent(new JobStatusTraceEvent("test_job", "fake_task_id", "fake_slave_id", JobStatusTraceEvent.Source.LITE_EXECUTOR, ExecutionType.READY, "0",
+ JobStatusTraceEvent.State.TASK_RUNNING, "message is empty.")));
+ }
+
+ @Test
+ public void assertAddJobStatusTraceEventWhenFailoverWithTaskStagingState() throws SQLException {
+ JobStatusTraceEvent jobStatusTraceEvent = new JobStatusTraceEvent("test_job", "fake_failover_task_id", "fake_slave_id", JobStatusTraceEvent.Source.LITE_EXECUTOR, ExecutionType.FAILOVER, "0",
+ JobStatusTraceEvent.State.TASK_STAGING, "message is empty.");
+ jobStatusTraceEvent.setOriginalTaskId("original_fake_failover_task_id");
+ assertThat(storage.getJobStatusTraceEvents("fake_failover_task_id").size(), is(0));
+ storage.addJobStatusTraceEvent(jobStatusTraceEvent);
+ assertThat(storage.getJobStatusTraceEvents("fake_failover_task_id").size(), is(1));
+ }
+
+ @Test
+ public void assertAddJobStatusTraceEventWhenFailoverWithTaskFailedState() throws SQLException {
+ JobStatusTraceEvent stagingJobStatusTraceEvent = new JobStatusTraceEvent("test_job", "fake_failed_failover_task_id", "fake_slave_id", JobStatusTraceEvent.Source.LITE_EXECUTOR, ExecutionType.FAILOVER, "0",
+ JobStatusTraceEvent.State.TASK_STAGING, "message is empty.");
+ stagingJobStatusTraceEvent.setOriginalTaskId("original_fake_failed_failover_task_id");
+ storage.addJobStatusTraceEvent(stagingJobStatusTraceEvent);
+ JobStatusTraceEvent failedJobStatusTraceEvent = new JobStatusTraceEvent("test_job", "fake_failed_failover_task_id", "fake_slave_id", JobStatusTraceEvent.Source.LITE_EXECUTOR, ExecutionType.FAILOVER, "0",
+ JobStatusTraceEvent.State.TASK_FAILED, "message is empty.");
+ storage.addJobStatusTraceEvent(failedJobStatusTraceEvent);
+ List<JobStatusTraceEvent> jobStatusTraceEvents = storage.getJobStatusTraceEvents("fake_failed_failover_task_id");
+ assertThat(jobStatusTraceEvents.size(), is(2));
+ for (JobStatusTraceEvent jobStatusTraceEvent : jobStatusTraceEvents) {
+ assertThat(jobStatusTraceEvent.getOriginalTaskId(), is("original_fake_failed_failover_task_id"));
+ }
+ }
+
+ @Test
+ public void assertUpdateJobExecutionEventWhenSuccess() throws SQLException {
+ JobExecutionEvent startEvent = new JobExecutionEvent("fake_task_id", "test_job", JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0);
+ assertTrue(storage.addJobExecutionEvent(startEvent));
+ JobExecutionEvent successEvent = startEvent.executionSuccess();
+ assertTrue(storage.addJobExecutionEvent(successEvent));
+ }
+
+ @Test
+ public void assertUpdateJobExecutionEventWhenFailure() throws SQLException {
+ JobExecutionEvent startEvent = new JobExecutionEvent("fake_task_id", "test_job", JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0);
+ assertTrue(storage.addJobExecutionEvent(startEvent));
+ JobExecutionEvent failureEvent = startEvent.executionFailure(new RuntimeException("failure"));
+ assertTrue(storage.addJobExecutionEvent(failureEvent));
+ assertThat(failureEvent.getFailureCause(), startsWith("java.lang.RuntimeException: failure"));
+ assertTrue(null != failureEvent.getCompleteTime());
+ }
+
+ @Test
+ public void assertUpdateJobExecutionEventWhenSuccessAndConflict() throws SQLException {
+ JobExecutionEvent startEvent = new JobExecutionEvent("fake_task_id", "test_job", JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0);
+ JobExecutionEvent successEvent = startEvent.executionSuccess();
+ assertTrue(storage.addJobExecutionEvent(successEvent));
+ assertFalse(storage.addJobExecutionEvent(startEvent));
+ }
+
+ @Test
+ public void assertUpdateJobExecutionEventWhenFailureAndConflict() throws SQLException {
+ JobExecutionEvent startEvent = new JobExecutionEvent("fake_task_id", "test_job", JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0);
+ JobExecutionEvent failureEvent = startEvent.executionFailure(new RuntimeException("failure"));
+ assertTrue(storage.addJobExecutionEvent(failureEvent));
+ assertThat(failureEvent.getFailureCause(), startsWith("java.lang.RuntimeException: failure"));
+ assertFalse(storage.addJobExecutionEvent(startEvent));
+ }
+
+ @Test
+ public void assertUpdateJobExecutionEventWhenFailureAndMessageExceed() throws SQLException {
+ JobExecutionEvent startEvent = new JobExecutionEvent("fake_task_id", "test_job", JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0);
+ assertTrue(storage.addJobExecutionEvent(startEvent));
+ StringBuilder failureMsg = new StringBuilder();
+ for (int i = 0; i < 600; i++) {
+ failureMsg.append(i);
+ }
+ JobExecutionEvent failEvent = startEvent.executionFailure(new RuntimeException("failure" + failureMsg.toString()));
+ assertTrue(storage.addJobExecutionEvent(failEvent));
+ assertThat(failEvent.getFailureCause(), startsWith("java.lang.RuntimeException: failure"));
+ }
+
+ @Test
+ public void assertFindJobExecutionEvent() throws SQLException {
+ storage.addJobExecutionEvent(new JobExecutionEvent("fake_task_id", "test_job", JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER, 0));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/AllExceptionTests.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/AllExceptionTests.java
new file mode 100644
index 0000000..387864b
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/AllExceptionTests.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.exception;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses({
+ JobConfigurationExceptionTest.class,
+ JobExecutionEnvironmentExceptionTest.class,
+ JobSystemExceptionTest.class,
+ ExceptionUtilTest.class,
+ JobStatisticExceptionTest.class
+ })
+public final class AllExceptionTests {
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/ExceptionUtilTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/ExceptionUtilTest.java
new file mode 100644
index 0000000..04c79ad
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/ExceptionUtilTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.exception;
+
+import org.junit.Test;
+
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public final class ExceptionUtilTest {
+
+ @Test
+ public void assertTransformWithError() {
+ assertTrue(ExceptionUtil.transform(new Error("Error")).startsWith("java.lang.Error"));
+ }
+
+ @Test
+ public void assertTransformWithException() {
+ assertTrue(ExceptionUtil.transform(new Exception("Exception")).startsWith("java.lang.Exception"));
+ }
+
+ @Test
+ public void assertTransformWithNull() {
+ assertThat(ExceptionUtil.transform(null), is(""));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/JobConfigurationExceptionTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/JobConfigurationExceptionTest.java
new file mode 100644
index 0000000..9c93547
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/JobConfigurationExceptionTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.exception;
+
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public final class JobConfigurationExceptionTest {
+
+ @Test
+ public void assertGetMessage() {
+ assertThat(new JobConfigurationException("message is: '%s'", "test").getMessage(), is("message is: 'test'"));
+ }
+
+ @Test
+ public void assertGetCause() {
+ assertThat(new JobConfigurationException(new RuntimeException()).getCause(), instanceOf(RuntimeException.class));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/JobExecutionEnvironmentExceptionTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/JobExecutionEnvironmentExceptionTest.java
new file mode 100644
index 0000000..f908923
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/JobExecutionEnvironmentExceptionTest.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.exception;
+
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public final class JobExecutionEnvironmentExceptionTest {
+
+ @Test
+ public void assertGetMessage() {
+ assertThat(new JobExecutionEnvironmentException("message is: '%s'", "test").getMessage(), is("message is: 'test'"));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/JobStatisticExceptionTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/JobStatisticExceptionTest.java
new file mode 100644
index 0000000..0b57e6e
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/JobStatisticExceptionTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.exception;
+
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertThat;
+
+public class JobStatisticExceptionTest {
+
+ @Test
+ public void assertGetCause() {
+ assertThat(new JobStatisticException(new RuntimeException()).getCause(), instanceOf(RuntimeException.class));
+ }
+
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/JobSystemExceptionTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/JobSystemExceptionTest.java
new file mode 100644
index 0000000..ca18c0c
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/exception/JobSystemExceptionTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.exception;
+
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public final class JobSystemExceptionTest {
+
+ @Test
+ public void assertGetMessage() {
+ assertThat(new JobSystemException("message is: '%s'", "test").getMessage(), is("message is: 'test'"));
+ }
+
+ @Test
+ public void assertGetCause() {
+ assertThat(new JobSystemException(new RuntimeException()).getCause(), instanceOf(RuntimeException.class));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/AllExecutorTests.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/AllExecutorTests.java
new file mode 100644
index 0000000..d93362c
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/AllExecutorTests.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor;
+
+import io.elasticjob.cloud.executor.handler.ExecutorServiceHandlerRegistryTest;
+import io.elasticjob.cloud.executor.handler.JobPropertiesTest;
+import io.elasticjob.cloud.executor.handler.impl.DefaultJobExceptionHandlerTest;
+import io.elasticjob.cloud.executor.type.DataflowJobExecutorTest;
+import io.elasticjob.cloud.executor.type.ScriptJobExecutorTest;
+import io.elasticjob.cloud.executor.type.SimpleJobExecutorTest;
+import io.elasticjob.cloud.executor.type.WrongJobExecutorTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses({
+ JobExecutorFactoryTest.class,
+ ExecutorServiceHandlerRegistryTest.class,
+ JobPropertiesTest.class,
+ DefaultJobExceptionHandlerTest.class,
+ SimpleJobExecutorTest.class,
+ WrongJobExecutorTest.class,
+ DataflowJobExecutorTest.class,
+ ScriptJobExecutorTest.class
+ })
+public final class AllExecutorTests {
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/JobExecutorFactoryTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/JobExecutorFactoryTest.java
new file mode 100644
index 0000000..b869a2a
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/JobExecutorFactoryTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor;
+
+import io.elasticjob.cloud.exception.JobConfigurationException;
+import io.elasticjob.cloud.executor.type.DataflowJobExecutor;
+import io.elasticjob.cloud.fixture.config.TestDataflowJobConfiguration;
+import io.elasticjob.cloud.fixture.config.TestScriptJobConfiguration;
+import io.elasticjob.cloud.fixture.config.TestSimpleJobConfiguration;
+import io.elasticjob.cloud.fixture.job.TestDataflowJob;
+import io.elasticjob.cloud.executor.type.ScriptJobExecutor;
+import io.elasticjob.cloud.executor.type.SimpleJobExecutor;
+import io.elasticjob.cloud.fixture.handler.IgnoreJobExceptionHandler;
+import io.elasticjob.cloud.fixture.job.OtherJob;
+import io.elasticjob.cloud.fixture.job.TestSimpleJob;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class JobExecutorFactoryTest {
+
+ @Mock
+ private JobFacade jobFacade;
+
+ @Test
+ public void assertGetJobExecutorForScriptJob() {
+ when(jobFacade.loadJobRootConfiguration(true)).thenReturn(new TestScriptJobConfiguration("test.sh", IgnoreJobExceptionHandler.class));
+ assertThat(JobExecutorFactory.getJobExecutor(null, jobFacade), instanceOf(ScriptJobExecutor.class));
+ }
+
+ @Test
+ public void assertGetJobExecutorForSimpleJob() {
+ when(jobFacade.loadJobRootConfiguration(true)).thenReturn(new TestSimpleJobConfiguration());
+ assertThat(JobExecutorFactory.getJobExecutor(new TestSimpleJob(null), jobFacade), instanceOf(SimpleJobExecutor.class));
+ }
+
+ @Test
+ public void assertGetJobExecutorForDataflowJob() {
+ when(jobFacade.loadJobRootConfiguration(true)).thenReturn(new TestDataflowJobConfiguration(false));
+ assertThat(JobExecutorFactory.getJobExecutor(new TestDataflowJob(null), jobFacade), instanceOf(DataflowJobExecutor.class));
+ }
+
+ @Test(expected = JobConfigurationException.class)
+ public void assertGetJobExecutorWhenJobClassWhenUnsupportedJob() {
+ JobExecutorFactory.getJobExecutor(new OtherJob(), jobFacade);
+ }
+
+ @Test
+ public void assertGetJobExecutorTwice() {
+ when(jobFacade.loadJobRootConfiguration(true)).thenReturn(new TestDataflowJobConfiguration(false));
+ AbstractElasticJobExecutor executor = JobExecutorFactory.getJobExecutor(new TestSimpleJob(null), jobFacade);
+ AbstractElasticJobExecutor anotherExecutor = JobExecutorFactory.getJobExecutor(new TestSimpleJob(null), jobFacade);
+ assertTrue(executor.hashCode() != anotherExecutor.hashCode());
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/handler/ExecutorServiceHandlerRegistryTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/handler/ExecutorServiceHandlerRegistryTest.java
new file mode 100644
index 0000000..37e621e
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/handler/ExecutorServiceHandlerRegistryTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.handler;
+
+import io.elasticjob.cloud.executor.handler.impl.DefaultExecutorServiceHandler;
+import lombok.RequiredArgsConstructor;
+import org.junit.After;
+import org.junit.Test;
+
+import java.util.Set;
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertThat;
+
+public final class ExecutorServiceHandlerRegistryTest {
+
+ @After
+ public void clear() {
+ ExecutorServiceHandlerRegistry.remove("test_job");
+ }
+
+ @Test
+ public void assertRemove() {
+ ExecutorService actual = ExecutorServiceHandlerRegistry.getExecutorServiceHandler("test_job", new DefaultExecutorServiceHandler());
+ ExecutorServiceHandlerRegistry.remove("test_job");
+ assertThat(actual, not(ExecutorServiceHandlerRegistry.getExecutorServiceHandler("test_job", new DefaultExecutorServiceHandler())));
+ }
+
+ @Test
+ public void assertGetExecutorServiceHandlerForSameThread() {
+ assertThat(ExecutorServiceHandlerRegistry.getExecutorServiceHandler("test_job", new DefaultExecutorServiceHandler()),
+ is(ExecutorServiceHandlerRegistry.getExecutorServiceHandler("test_job", new DefaultExecutorServiceHandler())));
+ }
+
+ @Test
+ public void assertGetExecutorServiceHandlerForConcurrent() throws InterruptedException {
+ int threadCount = 100;
+ CyclicBarrier barrier = new CyclicBarrier(threadCount);
+ ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ Set<ExecutorService> set = new CopyOnWriteArraySet<>();
+ for (int i = 0; i < threadCount; i++) {
+ executorService.submit(new GetExecutorServiceHandlerTask(barrier, latch, set));
+ }
+ latch.await();
+ assertThat(set.size(), is(1));
+ assertThat(ExecutorServiceHandlerRegistry.getExecutorServiceHandler("test_job", new DefaultExecutorServiceHandler()), is(set.iterator().next()));
+ }
+
+ @RequiredArgsConstructor
+ class GetExecutorServiceHandlerTask implements Runnable {
+
+ private final CyclicBarrier barrier;
+
+ private final CountDownLatch latch;
+
+ private final Set<ExecutorService> set;
+
+ @Override
+ public void run() {
+ try {
+ barrier.await();
+ } catch (final InterruptedException | BrokenBarrierException ex) {
+ ex.printStackTrace();
+ }
+ set.add(ExecutorServiceHandlerRegistry.getExecutorServiceHandler("test_job", new DefaultExecutorServiceHandler()));
+ latch.countDown();
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/handler/JobPropertiesTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/handler/JobPropertiesTest.java
new file mode 100644
index 0000000..2834bcb
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/handler/JobPropertiesTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.handler;
+
+import io.elasticjob.cloud.executor.handler.impl.DefaultExecutorServiceHandler;
+import io.elasticjob.cloud.executor.handler.impl.DefaultJobExceptionHandler;
+import io.elasticjob.cloud.fixture.APIJsonConstants;
+import io.elasticjob.cloud.fixture.handler.IgnoreJobExceptionHandler;
+import org.junit.Test;
+import org.unitils.util.ReflectionUtils;
+
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public final class JobPropertiesTest {
+
+ @Test
+ public void assertPutInvalidKey() throws NoSuchFieldException {
+ JobProperties actual = new JobProperties();
+ actual.put("invalid_key", "");
+ assertTrue(getMap(actual).isEmpty());
+ }
+
+ @Test
+ public void assertPutNullValue() throws NoSuchFieldException {
+ JobProperties actual = new JobProperties();
+ actual.put(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER.getKey(), null);
+ assertTrue(getMap(actual).isEmpty());
+ }
+
+ @Test
+ public void assertPutSuccess() throws NoSuchFieldException {
+ JobProperties actual = new JobProperties();
+ actual.put(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER.getKey(), DefaultJobExceptionHandler.class.getCanonicalName());
+ assertThat(getMap(actual).size(), is(1));
+ }
+
+ private Map getMap(final JobProperties jobProperties) throws NoSuchFieldException {
+ return (Map) ReflectionUtils.getFieldValue(jobProperties, JobProperties.class.getDeclaredField("map"));
+ }
+
+ @Test
+ public void assertGetWhenValueIsEmpty() throws NoSuchFieldException {
+ JobProperties actual = new JobProperties();
+ assertThat(actual.get(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER), is(DefaultJobExceptionHandler.class.getCanonicalName()));
+ assertThat(actual.get(JobProperties.JobPropertiesEnum.EXECUTOR_SERVICE_HANDLER), is(DefaultExecutorServiceHandler.class.getCanonicalName()));
+ }
+
+ @Test
+ public void assertGetWhenValueIsNotEmpty() throws NoSuchFieldException {
+ JobProperties actual = new JobProperties();
+ actual.put(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER.getKey(), IgnoreJobExceptionHandler.class.getCanonicalName());
+ assertThat(actual.get(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER), is(IgnoreJobExceptionHandler.class.getCanonicalName()));
+ }
+
+ @Test
+ public void assertJson() {
+ assertThat(new JobProperties().json(), is(APIJsonConstants.getJobPropertiesJson(DefaultJobExceptionHandler.class.getCanonicalName())));
+ }
+
+ @Test
+ public void assertJobPropertiesEnumFromValidValue() {
+ assertThat(JobProperties.JobPropertiesEnum.from(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER.getKey()), is(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER));
+ }
+
+ @Test
+ public void assertJobPropertiesEnumFromInvalidValue() {
+ assertNull(JobProperties.JobPropertiesEnum.from("invalid"));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/handler/impl/DefaultJobExceptionHandlerTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/handler/impl/DefaultJobExceptionHandlerTest.java
new file mode 100644
index 0000000..fcbb718
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/handler/impl/DefaultJobExceptionHandlerTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.handler.impl;
+
+import io.elasticjob.cloud.event.fixture.JobEventCaller;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static org.mockito.Mockito.atMost;
+import static org.mockito.Mockito.verify;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class DefaultJobExceptionHandlerTest {
+
+ @Mock
+ private JobEventCaller caller;
+
+ @Test
+ public void assertHandleException() {
+ new DefaultJobExceptionHandler().handleException("test_job", new RuntimeException("test"));
+ verify(caller, atMost(1)).call();
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/DataflowJobExecutorTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/DataflowJobExecutorTest.java
new file mode 100644
index 0000000..29573db
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/DataflowJobExecutorTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.type;
+
+import io.elasticjob.cloud.executor.JobFacade;
+import io.elasticjob.cloud.executor.ShardingContexts;
+import io.elasticjob.cloud.fixture.ShardingContextsBuilder;
+import io.elasticjob.cloud.fixture.config.TestDataflowJobConfiguration;
+import io.elasticjob.cloud.fixture.job.JobCaller;
+import io.elasticjob.cloud.fixture.job.TestDataflowJob;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class DataflowJobExecutorTest {
+
+ @Mock
+ private JobCaller jobCaller;
+
+ @Mock
+ private JobFacade jobFacade;
+
+ private ShardingContexts shardingContexts;
+
+ private DataflowJobExecutor dataflowJobExecutor;
+
+ @After
+ public void tearDown() throws NoSuchFieldException {
+ verify(jobFacade).loadJobRootConfiguration(true);
+ ElasticJobVerify.verifyForIsNotMisfire(jobFacade, shardingContexts);
+ }
+
+ @Test
+ public void assertExecuteWhenFetchDataIsNullAndEmpty() {
+ setUp(true, ShardingContextsBuilder.getMultipleShardingContexts());
+ when(jobCaller.fetchData(0)).thenReturn(null);
+ when(jobCaller.fetchData(1)).thenReturn(Collections.emptyList());
+ dataflowJobExecutor.execute();
+ verify(jobCaller).fetchData(0);
+ verify(jobCaller).fetchData(1);
+ verify(jobCaller, times(0)).processData(any());
+ }
+
+ @Test
+ public void assertExecuteWhenFetchDataIsNotEmptyForUnStreamingProcessAndSingleShardingItem() {
+ setUp(false, ShardingContextsBuilder.getSingleShardingContexts());
+ doThrow(new IllegalStateException()).when(jobCaller).fetchData(0);
+ dataflowJobExecutor.execute();
+ verify(jobCaller).fetchData(0);
+ verify(jobCaller, times(0)).processData(any());
+ }
+
+ @Test
+ public void assertExecuteWhenFetchDataIsNotEmptyForUnStreamingProcessAndMultipleShardingItems() {
+ setUp(false, ShardingContextsBuilder.getMultipleShardingContexts());
+ when(jobCaller.fetchData(0)).thenReturn(Arrays.<Object>asList(1, 2));
+ when(jobCaller.fetchData(1)).thenReturn(Arrays.<Object>asList(3, 4));
+ doThrow(new IllegalStateException()).when(jobCaller).processData(4);
+ dataflowJobExecutor.execute();
+ verify(jobCaller).fetchData(0);
+ verify(jobCaller).fetchData(1);
+ verify(jobCaller).processData(1);
+ verify(jobCaller).processData(2);
+ verify(jobCaller).processData(3);
+ verify(jobCaller).processData(4);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void assertExecuteWhenFetchDataIsNotEmptyForStreamingProcessAndSingleShardingItem() {
+ setUp(true, ShardingContextsBuilder.getSingleShardingContexts());
+ when(jobCaller.fetchData(0)).thenReturn(Collections.<Object>singletonList(1), Collections.emptyList());
+ when(jobFacade.isEligibleForJobRunning()).thenReturn(true);
+ dataflowJobExecutor.execute();
+ verify(jobCaller, times(2)).fetchData(0);
+ verify(jobCaller).processData(1);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void assertExecuteWhenFetchDataIsNotEmptyForStreamingProcessAndMultipleShardingItems() {
+ setUp(true, ShardingContextsBuilder.getMultipleShardingContexts());
+ when(jobCaller.fetchData(0)).thenReturn(Collections.<Object>singletonList(1), Collections.emptyList());
+ when(jobCaller.fetchData(1)).thenReturn(Collections.<Object>singletonList(2), Collections.emptyList());
+ when(jobFacade.isEligibleForJobRunning()).thenReturn(true);
+ dataflowJobExecutor.execute();
+ verify(jobCaller, times(2)).fetchData(0);
+ verify(jobCaller, times(2)).fetchData(1);
+ verify(jobCaller).processData(1);
+ verify(jobCaller).processData(2);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void assertExecuteWhenFetchDataIsNotEmptyAndProcessFailureWithExceptionForStreamingProcess() {
+ setUp(true, ShardingContextsBuilder.getMultipleShardingContexts());
+ when(jobCaller.fetchData(0)).thenReturn(Collections.<Object>singletonList(1), Collections.emptyList());
+ when(jobCaller.fetchData(1)).thenReturn(Arrays.<Object>asList(2, 3), Collections.emptyList());
+ when(jobFacade.isEligibleForJobRunning()).thenReturn(true);
+ doThrow(new IllegalStateException()).when(jobCaller).processData(2);
+ dataflowJobExecutor.execute();
+ verify(jobCaller, times(2)).fetchData(0);
+ verify(jobCaller, times(1)).fetchData(1);
+ verify(jobCaller).processData(1);
+ verify(jobCaller).processData(2);
+ verify(jobCaller, times(0)).processData(3);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void assertExecuteWhenFetchDataIsNotEmptyAndIsEligibleForJobRunningForStreamingProcess() {
+ setUp(true, ShardingContextsBuilder.getMultipleShardingContexts());
+ when(jobFacade.isEligibleForJobRunning()).thenReturn(true);
+ when(jobCaller.fetchData(0)).thenReturn(Arrays.<Object>asList(1, 2), Collections.emptyList());
+ when(jobCaller.fetchData(1)).thenReturn(Arrays.<Object>asList(3, 4), Collections.emptyList());
+ doThrow(new IllegalStateException()).when(jobCaller).processData(4);
+ dataflowJobExecutor.execute();
+ verify(jobCaller, times(2)).fetchData(0);
+ verify(jobCaller, times(1)).fetchData(1);
+ verify(jobCaller).processData(1);
+ verify(jobCaller).processData(2);
+ verify(jobCaller).processData(3);
+ verify(jobCaller).processData(4);
+ }
+
+ @Test
+ public void assertExecuteWhenFetchDataIsNotEmptyAndIsNotEligibleForJobRunningForStreamingProcess() {
+ setUp(true, ShardingContextsBuilder.getMultipleShardingContexts());
+ when(jobFacade.isEligibleForJobRunning()).thenReturn(false);
+ when(jobCaller.fetchData(0)).thenReturn(Arrays.<Object>asList(1, 2));
+ when(jobCaller.fetchData(1)).thenReturn(Arrays.<Object>asList(3, 4));
+ doThrow(new IllegalStateException()).when(jobCaller).processData(4);
+ dataflowJobExecutor.execute();
+ verify(jobCaller).fetchData(0);
+ verify(jobCaller).fetchData(1);
+ verify(jobCaller).processData(1);
+ verify(jobCaller).processData(2);
+ verify(jobCaller).processData(3);
+ verify(jobCaller).processData(4);
+ }
+
+ private void setUp(final boolean isStreamingProcess, final ShardingContexts shardingContexts) {
+ this.shardingContexts = shardingContexts;
+ when(jobFacade.loadJobRootConfiguration(true)).thenReturn(new TestDataflowJobConfiguration(isStreamingProcess));
+ when(jobFacade.getShardingContexts()).thenReturn(shardingContexts);
+ dataflowJobExecutor = new DataflowJobExecutor(new TestDataflowJob(jobCaller), jobFacade);
+ ElasticJobVerify.prepareForIsNotMisfire(jobFacade, shardingContexts);
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/ElasticJobVerify.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/ElasticJobVerify.java
new file mode 100644
index 0000000..16b4157
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/ElasticJobVerify.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.type;
+
+import io.elasticjob.cloud.executor.ShardingContexts;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+import io.elasticjob.cloud.exception.JobExecutionEnvironmentException;
+import io.elasticjob.cloud.executor.JobFacade;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+final class ElasticJobVerify {
+
+ public static void prepareForIsNotMisfire(final JobFacade jobFacade, final ShardingContexts shardingContexts) {
+ when(jobFacade.getShardingContexts()).thenReturn(shardingContexts);
+ when(jobFacade.misfireIfRunning(shardingContexts.getShardingItemParameters().keySet())).thenReturn(false);
+ when(jobFacade.isExecuteMisfired(shardingContexts.getShardingItemParameters().keySet())).thenReturn(false);
+ }
+
+ public static void verifyForIsNotMisfire(final JobFacade jobFacade, final ShardingContexts shardingContexts) {
+ try {
+ verify(jobFacade).checkJobExecutionEnvironment();
+ } catch (final JobExecutionEnvironmentException ex) {
+ throw new RuntimeException(ex);
+ }
+ verify(jobFacade).getShardingContexts();
+ verify(jobFacade).postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_STAGING, "Job 'test_job' execute begin.");
+ verify(jobFacade).misfireIfRunning(shardingContexts.getShardingItemParameters().keySet());
+ verify(jobFacade).beforeJobExecuted(shardingContexts);
+ verify(jobFacade).registerJobBegin(shardingContexts);
+ verify(jobFacade).registerJobCompleted(shardingContexts);
+ verify(jobFacade).isExecuteMisfired(shardingContexts.getShardingItemParameters().keySet());
+ verify(jobFacade).afterJobExecuted(shardingContexts);
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/ScriptJobExecutorTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/ScriptJobExecutorTest.java
new file mode 100644
index 0000000..7f2403b
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/ScriptJobExecutorTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.type;
+
+import io.elasticjob.cloud.executor.ShardingContexts;
+import io.elasticjob.cloud.fixture.config.TestScriptJobConfiguration;
+import io.elasticjob.cloud.exception.JobSystemException;
+import io.elasticjob.cloud.executor.JobFacade;
+import io.elasticjob.cloud.fixture.ShardingContextsBuilder;
+import io.elasticjob.cloud.fixture.handler.IgnoreJobExceptionHandler;
+import io.elasticjob.cloud.fixture.handler.ThrowJobExceptionHandler;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.io.IOException;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class ScriptJobExecutorTest {
+
+ @Mock
+ private JobFacade jobFacade;
+
+ private ScriptJobExecutor scriptJobExecutor;
+
+ @Test
+ public void assertExecuteWhenCommandLineIsEmpty() throws IOException {
+ ElasticJobVerify.prepareForIsNotMisfire(jobFacade, ShardingContextsBuilder.getMultipleShardingContexts());
+ when(jobFacade.loadJobRootConfiguration(true)).thenReturn(new TestScriptJobConfiguration("", IgnoreJobExceptionHandler.class));
+ scriptJobExecutor = new ScriptJobExecutor(jobFacade);
+ scriptJobExecutor.execute();
+ }
+
+ @Test(expected = JobSystemException.class)
+ public void assertExecuteWhenExecuteFailureForSingleShardingItems() throws IOException, NoSuchFieldException {
+ assertExecuteWhenExecuteFailure(ShardingContextsBuilder.getSingleShardingContexts());
+ }
+
+ @Test
+ public void assertExecuteWhenExecuteFailureForMultipleShardingItems() throws IOException, NoSuchFieldException {
+ assertExecuteWhenExecuteFailure(ShardingContextsBuilder.getMultipleShardingContexts());
+ }
+
+ private void assertExecuteWhenExecuteFailure(final ShardingContexts shardingContexts) {
+ ElasticJobVerify.prepareForIsNotMisfire(jobFacade, shardingContexts);
+ when(jobFacade.loadJobRootConfiguration(true)).thenReturn(new TestScriptJobConfiguration("not_exists_file", ThrowJobExceptionHandler.class));
+ scriptJobExecutor = new ScriptJobExecutor(jobFacade);
+ scriptJobExecutor.execute();
+ }
+
+ @Test
+ public void assertExecuteSuccessForMultipleShardingItems() throws IOException, NoSuchFieldException {
+ assertExecuteSuccess(ShardingContextsBuilder.getMultipleShardingContexts());
+ }
+
+ @Test
+ public void assertExecuteSuccessForSingleShardingItems() throws IOException, NoSuchFieldException {
+ assertExecuteSuccess(ShardingContextsBuilder.getSingleShardingContexts());
+ }
+
+ private void assertExecuteSuccess(final ShardingContexts shardingContexts) throws IOException, NoSuchFieldException {
+ ElasticJobVerify.prepareForIsNotMisfire(jobFacade, shardingContexts);
+ when(jobFacade.loadJobRootConfiguration(true)).thenReturn(new TestScriptJobConfiguration("exists_file param0 param1", IgnoreJobExceptionHandler.class));
+ scriptJobExecutor = new ScriptJobExecutor(jobFacade);
+ scriptJobExecutor.execute();
+ verify(jobFacade).loadJobRootConfiguration(true);
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/SimpleJobExecutorTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/SimpleJobExecutorTest.java
new file mode 100644
index 0000000..13d2b78
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/SimpleJobExecutorTest.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.type;
+
+import io.elasticjob.cloud.executor.AbstractElasticJobExecutor;
+import io.elasticjob.cloud.executor.ShardingContexts;
+import io.elasticjob.cloud.fixture.config.TestSimpleJobConfiguration;
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+import io.elasticjob.cloud.exception.JobExecutionEnvironmentException;
+import io.elasticjob.cloud.exception.JobSystemException;
+import io.elasticjob.cloud.executor.JobFacade;
+import io.elasticjob.cloud.executor.handler.impl.DefaultExecutorServiceHandler;
+import io.elasticjob.cloud.executor.handler.impl.DefaultJobExceptionHandler;
+import io.elasticjob.cloud.fixture.ShardingContextsBuilder;
+import io.elasticjob.cloud.fixture.job.JobCaller;
+import io.elasticjob.cloud.fixture.job.TestSimpleJob;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.unitils.util.ReflectionUtils;
+
+import java.util.Collections;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class SimpleJobExecutorTest {
+
+ @Mock
+ private JobCaller jobCaller;
+
+ @Mock
+ private JobFacade jobFacade;
+
+ private SimpleJobExecutor simpleJobExecutor;
+
+ @Before
+ public void setUp() throws NoSuchFieldException {
+ when(jobFacade.loadJobRootConfiguration(true)).thenReturn(new TestSimpleJobConfiguration());
+ simpleJobExecutor = new SimpleJobExecutor(new TestSimpleJob(jobCaller), jobFacade);
+ }
+
+ @Test
+ public void assertNewExecutorWithDefaultHandlers() throws NoSuchFieldException {
+ when(jobFacade.loadJobRootConfiguration(true)).thenReturn(new TestSimpleJobConfiguration("ErrorHandler", Object.class.getName()));
+ SimpleJobExecutor simpleJobExecutor = new SimpleJobExecutor(new TestSimpleJob(jobCaller), jobFacade);
+ assertThat(ReflectionUtils.getFieldValue(simpleJobExecutor, AbstractElasticJobExecutor.class.getDeclaredField("executorService")),
+ instanceOf(new DefaultExecutorServiceHandler().createExecutorService("test_job").getClass()));
+ assertThat(ReflectionUtils.getFieldValue(simpleJobExecutor, AbstractElasticJobExecutor.class.getDeclaredField("jobExceptionHandler")),
+ instanceOf(DefaultJobExceptionHandler.class));
+ }
+
+ @Test(expected = JobSystemException.class)
+ public void assertExecuteWhenCheckMaxTimeDiffSecondsIntolerable() throws JobExecutionEnvironmentException {
+ doThrow(JobExecutionEnvironmentException.class).when(jobFacade).checkJobExecutionEnvironment();
+ try {
+ simpleJobExecutor.execute();
+ } finally {
+ verify(jobFacade).checkJobExecutionEnvironment();
+ verify(jobCaller, times(0)).execute();
+ }
+ }
+
+ @Test
+ public void assertExecuteWhenPreviousJobStillRunning() throws JobExecutionEnvironmentException {
+ ShardingContexts shardingContexts = new ShardingContexts("fake_task_id", "test_job", 10, "", Collections.<Integer, String>emptyMap());
+ when(jobFacade.getShardingContexts()).thenReturn(shardingContexts);
+ when(jobFacade.misfireIfRunning(shardingContexts.getShardingItemParameters().keySet())).thenReturn(true);
+ simpleJobExecutor.execute();
+ verify(jobFacade).postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_STAGING, "Job 'test_job' execute begin.");
+ verify(jobFacade).postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_FINISHED,
+ "Previous job 'test_job' - shardingItems '[]' is still running, misfired job will start after previous job completed.");
+ verify(jobFacade).checkJobExecutionEnvironment();
+ verify(jobFacade).getShardingContexts();
+ verify(jobFacade).misfireIfRunning(shardingContexts.getShardingItemParameters().keySet());
+ verify(jobCaller, times(0)).execute();
+ }
+
+ @Test
+ public void assertExecuteWhenShardingItemsIsEmpty() throws JobExecutionEnvironmentException {
+ ShardingContexts shardingContexts = new ShardingContexts("fake_task_id", "test_job", 10, "", Collections.<Integer, String>emptyMap());
+ ElasticJobVerify.prepareForIsNotMisfire(jobFacade, shardingContexts);
+ simpleJobExecutor.execute();
+ verify(jobFacade).postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_STAGING, "Job 'test_job' execute begin.");
+ verify(jobFacade).postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_FINISHED, "Sharding item for job 'test_job' is empty.");
+ verify(jobFacade).checkJobExecutionEnvironment();
+ verify(jobFacade).getShardingContexts();
+ verify(jobFacade).misfireIfRunning(shardingContexts.getShardingItemParameters().keySet());
+ verify(jobCaller, times(0)).execute();
+ }
+
+ @Test(expected = JobSystemException.class)
+ public void assertExecuteWhenRunOnceAndThrowExceptionForSingleShardingItem() throws JobExecutionEnvironmentException {
+ assertExecuteWhenRunOnceAndThrowException(ShardingContextsBuilder.getSingleShardingContexts());
+ }
+
+ @Test
+ public void assertExecuteWhenRunOnceAndThrowExceptionForMultipleShardingItems() throws JobExecutionEnvironmentException {
+ assertExecuteWhenRunOnceAndThrowException(ShardingContextsBuilder.getMultipleShardingContexts());
+ }
+
+ private void assertExecuteWhenRunOnceAndThrowException(final ShardingContexts shardingContexts) throws JobExecutionEnvironmentException {
+ ElasticJobVerify.prepareForIsNotMisfire(jobFacade, shardingContexts);
+ doThrow(RuntimeException.class).when(jobCaller).execute();
+ try {
+ simpleJobExecutor.execute();
+ } finally {
+ verify(jobFacade).postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_STAGING, "Job 'test_job' execute begin.");
+ verify(jobFacade).postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_RUNNING, "");
+ String errorMessage;
+ String lineSeparator = System.getProperty("line.separator");
+ if (1 == shardingContexts.getShardingItemParameters().size()) {
+ errorMessage = "{0=java.lang.RuntimeException" + lineSeparator + "}";
+ } else {
+ errorMessage = "{0=java.lang.RuntimeException" + lineSeparator + ", 1=java.lang.RuntimeException" + lineSeparator + "}";
+ }
+ verify(jobFacade).postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_ERROR, errorMessage);
+ verify(jobFacade).checkJobExecutionEnvironment();
+ verify(jobFacade).getShardingContexts();
+ verify(jobFacade).misfireIfRunning(shardingContexts.getShardingItemParameters().keySet());
+ verify(jobFacade).registerJobBegin(shardingContexts);
+ verify(jobCaller, times(shardingContexts.getShardingTotalCount())).execute();
+ verify(jobFacade).registerJobCompleted(shardingContexts);
+ }
+ }
+
+ @Test
+ public void assertExecuteWhenRunOnceSuccessForSingleShardingItems() {
+ assertExecuteWhenRunOnceSuccess(ShardingContextsBuilder.getSingleShardingContexts());
+ }
+
+ @Test
+ public void assertExecuteWhenRunOnceSuccessForMultipleShardingItems() {
+ assertExecuteWhenRunOnceSuccess(ShardingContextsBuilder.getMultipleShardingContexts());
+ }
+
+ private void assertExecuteWhenRunOnceSuccess(final ShardingContexts shardingContexts) {
+ ElasticJobVerify.prepareForIsNotMisfire(jobFacade, shardingContexts);
+ simpleJobExecutor.execute();
+ verify(jobFacade).postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_STAGING, "Job 'test_job' execute begin.");
+ verify(jobFacade).postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_FINISHED, "");
+ ElasticJobVerify.verifyForIsNotMisfire(jobFacade, shardingContexts);
+ verify(jobCaller, times(shardingContexts.getShardingTotalCount())).execute();
+ }
+
+ @Test
+ public void assertExecuteWhenRunOnceWithMisfireIsEmpty() {
+ ShardingContexts shardingContexts = ShardingContextsBuilder.getMultipleShardingContexts();
+ when(jobFacade.getShardingContexts()).thenReturn(shardingContexts);
+ when(jobFacade.isExecuteMisfired(shardingContexts.getShardingItemParameters().keySet())).thenReturn(false);
+ simpleJobExecutor.execute();
+ ElasticJobVerify.verifyForIsNotMisfire(jobFacade, shardingContexts);
+ verify(jobCaller, times(2)).execute();
+ }
+
+ @Test
+ public void assertExecuteWhenRunOnceWithMisfireIsNotEmptyButIsNotEligibleForJobRunning() {
+ ShardingContexts shardingContexts = ShardingContextsBuilder.getMultipleShardingContexts();
+ when(jobFacade.getShardingContexts()).thenReturn(shardingContexts);
+ when(jobFacade.isExecuteMisfired(shardingContexts.getShardingItemParameters().keySet())).thenReturn(false);
+ simpleJobExecutor.execute();
+ ElasticJobVerify.verifyForIsNotMisfire(jobFacade, shardingContexts);
+ verify(jobCaller, times(2)).execute();
+ verify(jobFacade, times(0)).clearMisfire(shardingContexts.getShardingItemParameters().keySet());
+ }
+
+ @Test
+ public void assertExecuteWhenRunOnceWithMisfire() throws JobExecutionEnvironmentException {
+ ShardingContexts shardingContexts = ShardingContextsBuilder.getMultipleShardingContexts();
+ when(jobFacade.getShardingContexts()).thenReturn(shardingContexts);
+ when(jobFacade.misfireIfRunning(shardingContexts.getShardingItemParameters().keySet())).thenReturn(false);
+ when(jobFacade.isExecuteMisfired(shardingContexts.getShardingItemParameters().keySet())).thenReturn(true, false);
+ simpleJobExecutor.execute();
+ verify(jobFacade).postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_STAGING, "Job 'test_job' execute begin.");
+ verify(jobFacade, times(2)).postJobStatusTraceEvent(shardingContexts.getTaskId(), JobStatusTraceEvent.State.TASK_RUNNING, "");
+ verify(jobFacade).checkJobExecutionEnvironment();
+ verify(jobFacade).getShardingContexts();
+ verify(jobFacade).misfireIfRunning(shardingContexts.getShardingItemParameters().keySet());
+ verify(jobFacade, times(2)).registerJobBegin(shardingContexts);
+ verify(jobCaller, times(4)).execute();
+ verify(jobFacade, times(2)).registerJobCompleted(shardingContexts);
+ }
+
+ @Test(expected = JobSystemException.class)
+ public void assertBeforeJobExecutedFailure() {
+ ShardingContexts shardingContexts = ShardingContextsBuilder.getMultipleShardingContexts();
+ when(jobFacade.getShardingContexts()).thenReturn(shardingContexts);
+ when(jobFacade.misfireIfRunning(shardingContexts.getShardingItemParameters().keySet())).thenReturn(false);
+ doThrow(RuntimeException.class).when(jobFacade).beforeJobExecuted(shardingContexts);
+ try {
+ simpleJobExecutor.execute();
+ } finally {
+ verify(jobCaller, times(0)).execute();
+ }
+ }
+
+ @Test(expected = JobSystemException.class)
+ public void assertAfterJobExecutedFailure() {
+ ShardingContexts shardingContexts = ShardingContextsBuilder.getMultipleShardingContexts();
+ when(jobFacade.getShardingContexts()).thenReturn(shardingContexts);
+ when(jobFacade.misfireIfRunning(shardingContexts.getShardingItemParameters().keySet())).thenReturn(false);
+ when(jobFacade.isExecuteMisfired(shardingContexts.getShardingItemParameters().keySet())).thenReturn(false);
+ doThrow(RuntimeException.class).when(jobFacade).afterJobExecuted(shardingContexts);
+ try {
+ simpleJobExecutor.execute();
+ } finally {
+ verify(jobCaller, times(2)).execute();
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/WrongJobExecutorTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/WrongJobExecutorTest.java
new file mode 100644
index 0000000..52d4889
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/executor/type/WrongJobExecutorTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.executor.type;
+
+import io.elasticjob.cloud.event.type.JobStatusTraceEvent;
+import io.elasticjob.cloud.executor.JobFacade;
+import io.elasticjob.cloud.executor.ShardingContexts;
+import io.elasticjob.cloud.fixture.config.TestSimpleJobConfiguration;
+import io.elasticjob.cloud.fixture.job.TestWrongJob;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class WrongJobExecutorTest {
+
+ @Mock
+ private JobFacade jobFacade;
+
+ private SimpleJobExecutor wrongSimpleJobExecutor;
+
+ @Before
+ public void setUp() throws NoSuchFieldException {
+ when(jobFacade.loadJobRootConfiguration(true)).thenReturn(new TestSimpleJobConfiguration());
+ wrongSimpleJobExecutor = new SimpleJobExecutor(new TestWrongJob(), jobFacade);
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void assertWrongJobExecutorWithSingleItem() throws NoSuchFieldException {
+ Map<Integer, String> map = new HashMap<>(1, 1);
+ map.put(0, "A");
+ ShardingContexts shardingContexts = new ShardingContexts("fake_task_id", "test_job", 10, "", map);
+ when(jobFacade.getShardingContexts()).thenReturn(shardingContexts);
+ wrongSimpleJobExecutor.execute();
+ }
+
+ @Test
+ public void assertWrongJobExecutorWithMultipleItems() throws NoSuchFieldException {
+ Map<Integer, String> map = new HashMap<>(1, 1);
+ map.put(0, "A");
+ map.put(1, "B");
+ ShardingContexts shardingContexts = new ShardingContexts("fake_task_id", "test_job", 10, "", map);
+ when(jobFacade.getShardingContexts()).thenReturn(shardingContexts);
+ wrongSimpleJobExecutor.execute();
+ verify(jobFacade).getShardingContexts();
+ verify(jobFacade).postJobStatusTraceEvent("fake_task_id", JobStatusTraceEvent.State.TASK_RUNNING, "");
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/APIJsonConstants.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/APIJsonConstants.java
new file mode 100644
index 0000000..91ae423
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/APIJsonConstants.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture;
+
+import io.elasticjob.cloud.executor.handler.impl.DefaultExecutorServiceHandler;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class APIJsonConstants {
+
+ private static final String JOB_PROPS_JSON = "{\"job_exception_handler\":\"%s\",\"executor_service_handler\":\"" + DefaultExecutorServiceHandler.class.getCanonicalName() + "\"}";
+
+ // CHECKSTYLE:OFF
+ private static final String SIMPLE_JOB_JSON = "{\"jobName\":\"test_job\",\"jobClass\":\"io.elasticjob.cloud.fixture.job.TestSimpleJob\",\"jobType\":\"SIMPLE\","
+ + "\"cron\":\"0/1 * * * * ?\",\"shardingTotalCount\":3,\"shardingItemParameters\":\"0\\u003dA,1\\u003dB,2\\u003dC\",\"jobParameter\":\"param\",\"failover\":true,\"misfire\":false,"
+ + "\"description\":\"desc\",\"jobProperties\":%s}";
+ // CHECKSTYLE:ON
+
+ private static final String DATAFLOW_JOB_JSON = "{\"jobName\":\"test_job\",\"jobClass\":\"io.elasticjob.cloud.fixture.job.TestDataflowJob\",\"jobType\":\"DATAFLOW\","
+ + "\"cron\":\"0/1 * * * * ?\",\"shardingTotalCount\":3,\"shardingItemParameters\":\"\",\"jobParameter\":\"\",\"failover\":false,\"misfire\":true,\"description\":\"\","
+ + "\"jobProperties\":%s,\"streamingProcess\":true}";
+
+ private static final String SCRIPT_JOB_JSON = "{\"jobName\":\"test_job\",\"jobClass\":\"io.elasticjob.cloud.api.script.ScriptJob\",\"jobType\":\"SCRIPT\",\"cron\":\"0/1 * * * * ?\","
+ + "\"shardingTotalCount\":3,\"shardingItemParameters\":\"\",\"jobParameter\":\"\",\"failover\":false,\"misfire\":true,\"description\":\"\","
+ + "\"jobProperties\":%s,\"scriptCommandLine\":\"test.sh\"}";
+
+ public static String getJobPropertiesJson(final String jobExceptionHandler) {
+ return String.format(JOB_PROPS_JSON, jobExceptionHandler);
+ }
+
+ public static String getSimpleJobJson(final String jobExceptionHandler) {
+ return String.format(SIMPLE_JOB_JSON, getJobPropertiesJson(jobExceptionHandler));
+ }
+
+ public static String getDataflowJobJson(final String jobExceptionHandler) {
+ return String.format(DATAFLOW_JOB_JSON, getJobPropertiesJson(jobExceptionHandler));
+ }
+
+ public static String getScriptJobJson(final String jobExceptionHandler) {
+ return String.format(SCRIPT_JOB_JSON, getJobPropertiesJson(jobExceptionHandler));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/EmbedTestingServer.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/EmbedTestingServer.java
new file mode 100644
index 0000000..646f109
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/EmbedTestingServer.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture;
+
+import com.google.common.base.Joiner;
+import io.elasticjob.cloud.reg.exception.RegExceptionHandler;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.curator.test.TestingServer;
+
+import java.io.File;
+import java.io.IOException;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class EmbedTestingServer {
+
+ private static final int PORT = 3181;
+
+ private static volatile TestingServer testingServer;
+
+ public static String getConnectionString() {
+ return Joiner.on(":").join("localhost", PORT);
+ }
+
+ public static void start() {
+ if (null != testingServer) {
+ return;
+ }
+ try {
+ testingServer = new TestingServer(PORT, new File(String.format("target/test_zk_data/%s/", System.nanoTime())));
+ // CHECKSTYLE:OFF
+ } catch (final Exception ex) {
+ // CHECKSTYLE:ON
+ RegExceptionHandler.handleException(ex);
+ } finally {
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+
+ @Override
+ public void run() {
+ try {
+ testingServer.close();
+ } catch (final IOException ex) {
+ RegExceptionHandler.handleException(ex);
+ }
+ }
+ });
+ }
+ }
+}
+
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/ShardingContextsBuilder.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/ShardingContextsBuilder.java
new file mode 100644
index 0000000..dad7100
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/ShardingContextsBuilder.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture;
+
+import io.elasticjob.cloud.executor.ShardingContexts;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+public final class ShardingContextsBuilder {
+
+ public static final String JOB_NAME = "test_job";
+
+ public static ShardingContexts getSingleShardingContexts() {
+ Map<Integer, String> map = new HashMap<>(1, 1);
+ map.put(0, "A");
+ return new ShardingContexts("fake_task_id", JOB_NAME, 1, "", map);
+ }
+
+ public static ShardingContexts getMultipleShardingContexts() {
+ Map<Integer, String> map = new HashMap<>(2, 1);
+ map.put(0, "A");
+ map.put(1, "B");
+ return new ShardingContexts("fake_task_id", JOB_NAME, 2, "", map);
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/config/TestDataflowJobConfiguration.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/config/TestDataflowJobConfiguration.java
new file mode 100644
index 0000000..2d85455
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/config/TestDataflowJobConfiguration.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture.config;
+
+import io.elasticjob.cloud.config.JobRootConfiguration;
+import io.elasticjob.cloud.config.JobTypeConfiguration;
+import io.elasticjob.cloud.fixture.job.TestDataflowJob;
+import io.elasticjob.cloud.config.JobCoreConfiguration;
+import io.elasticjob.cloud.config.dataflow.DataflowJobConfiguration;
+import io.elasticjob.cloud.executor.handler.JobProperties;
+import io.elasticjob.cloud.fixture.ShardingContextsBuilder;
+import io.elasticjob.cloud.fixture.handler.IgnoreJobExceptionHandler;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public final class TestDataflowJobConfiguration implements JobRootConfiguration {
+
+ private final boolean streamingProcess;
+
+ @Override
+ public JobTypeConfiguration getTypeConfig() {
+ return new DataflowJobConfiguration(JobCoreConfiguration.newBuilder(ShardingContextsBuilder.JOB_NAME, "0/1 * * * * ?", 3)
+ .jobProperties(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER.getKey(), IgnoreJobExceptionHandler.class.getCanonicalName()).build(),
+ TestDataflowJob.class.getCanonicalName(), streamingProcess);
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/config/TestJobRootConfiguration.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/config/TestJobRootConfiguration.java
new file mode 100644
index 0000000..1a18881
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/config/TestJobRootConfiguration.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture.config;
+
+import io.elasticjob.cloud.config.JobTypeConfiguration;
+import io.elasticjob.cloud.config.JobRootConfiguration;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public final class TestJobRootConfiguration implements JobRootConfiguration {
+
+ private final JobTypeConfiguration typeConfig;
+
+ @Override
+ public JobTypeConfiguration getTypeConfig() {
+ return typeConfig;
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/config/TestScriptJobConfiguration.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/config/TestScriptJobConfiguration.java
new file mode 100644
index 0000000..1b324a2
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/config/TestScriptJobConfiguration.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture.config;
+
+import io.elasticjob.cloud.config.JobTypeConfiguration;
+import io.elasticjob.cloud.config.script.ScriptJobConfiguration;
+import io.elasticjob.cloud.executor.handler.JobExceptionHandler;
+import io.elasticjob.cloud.config.JobCoreConfiguration;
+import io.elasticjob.cloud.config.JobRootConfiguration;
+import io.elasticjob.cloud.executor.handler.JobProperties;
+import io.elasticjob.cloud.fixture.ShardingContextsBuilder;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public final class TestScriptJobConfiguration implements JobRootConfiguration {
+
+ private final String scriptCommandLine;
+
+ private final Class<? extends JobExceptionHandler> jobExceptionHandlerClass;
+
+ @Override
+ public JobTypeConfiguration getTypeConfig() {
+ return new ScriptJobConfiguration(JobCoreConfiguration.newBuilder(ShardingContextsBuilder.JOB_NAME, "0/1 * * * * ?", 3)
+ .jobProperties(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER.getKey(), jobExceptionHandlerClass.getCanonicalName()).build(), scriptCommandLine);
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/config/TestSimpleJobConfiguration.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/config/TestSimpleJobConfiguration.java
new file mode 100644
index 0000000..a9878f5
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/config/TestSimpleJobConfiguration.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture.config;
+
+import io.elasticjob.cloud.config.JobCoreConfiguration;
+import io.elasticjob.cloud.config.JobRootConfiguration;
+import io.elasticjob.cloud.config.JobTypeConfiguration;
+import io.elasticjob.cloud.config.simple.SimpleJobConfiguration;
+import io.elasticjob.cloud.executor.handler.JobProperties;
+import io.elasticjob.cloud.fixture.ShardingContextsBuilder;
+import io.elasticjob.cloud.fixture.handler.ThrowJobExceptionHandler;
+import io.elasticjob.cloud.fixture.job.TestSimpleJob;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+public final class TestSimpleJobConfiguration implements JobRootConfiguration {
+
+ private String jobExceptionHandlerClassName;
+
+ private String executorServiceHandlerClassName;
+
+ public TestSimpleJobConfiguration(final String jobExceptionHandlerClassName, final String executorServiceHandlerClassName) {
+ this.jobExceptionHandlerClassName = jobExceptionHandlerClassName;
+ this.executorServiceHandlerClassName = executorServiceHandlerClassName;
+ }
+
+ @Override
+ public JobTypeConfiguration getTypeConfig() {
+ JobCoreConfiguration.Builder builder = JobCoreConfiguration.newBuilder(ShardingContextsBuilder.JOB_NAME, "0/1 * * * * ?", 3)
+ .shardingItemParameters("0=A,1=B,2=C").jobParameter("param").failover(true).misfire(false).description("desc");
+ if (null == jobExceptionHandlerClassName) {
+ builder.jobProperties(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER.getKey(), ThrowJobExceptionHandler.class.getCanonicalName());
+ } else {
+ builder.jobProperties(JobProperties.JobPropertiesEnum.JOB_EXCEPTION_HANDLER.getKey(), jobExceptionHandlerClassName);
+ }
+ if (null != executorServiceHandlerClassName) {
+ builder.jobProperties(JobProperties.JobPropertiesEnum.EXECUTOR_SERVICE_HANDLER.getKey(), executorServiceHandlerClassName);
+ }
+ return new SimpleJobConfiguration(builder.build(), TestSimpleJob.class.getCanonicalName());
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/context/TaskNode.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/context/TaskNode.java
new file mode 100644
index 0000000..820b5ee
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/context/TaskNode.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture.context;
+
+import com.google.common.base.Joiner;
+import io.elasticjob.cloud.context.ExecutionType;
+import lombok.Builder;
+
+@Builder
+public final class TaskNode {
+
+ private String jobName;
+
+ private int shardingItem;
+
+ private ExecutionType type;
+
+ private String slaveId;
+
+ private String uuid;
+
+ public String getTaskNodePath() {
+ return Joiner.on("@-@").join(null == jobName ? "test_job" : jobName, shardingItem);
+ }
+
+ public String getTaskNodeValue() {
+ return Joiner.on("@-@").join(getTaskNodePath(), null == type ? ExecutionType.READY : type, null == slaveId ? "slave-S0" : slaveId, null == uuid ? "0" : uuid);
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/handler/IgnoreJobExceptionHandler.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/handler/IgnoreJobExceptionHandler.java
new file mode 100644
index 0000000..224d00e
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/handler/IgnoreJobExceptionHandler.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture.handler;
+
+import io.elasticjob.cloud.executor.handler.JobExceptionHandler;
+
+public final class IgnoreJobExceptionHandler implements JobExceptionHandler {
+
+ @Override
+ public void handleException(final String jobName, final Throwable cause) {
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/handler/ThrowJobExceptionHandler.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/handler/ThrowJobExceptionHandler.java
new file mode 100644
index 0000000..6bae5c1
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/handler/ThrowJobExceptionHandler.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture.handler;
+
+import io.elasticjob.cloud.executor.handler.JobExceptionHandler;
+import io.elasticjob.cloud.exception.JobSystemException;
+
+public final class ThrowJobExceptionHandler implements JobExceptionHandler {
+
+ @Override
+ public void handleException(final String jobName, final Throwable cause) {
+ throw new JobSystemException(cause);
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/JobCaller.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/JobCaller.java
new file mode 100644
index 0000000..217a8a6
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/JobCaller.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture.job;
+
+import java.util.List;
+
+public interface JobCaller {
+
+ void execute();
+
+ List<Object> fetchData(int shardingItem);
+
+ void processData(Object data);
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/OtherJob.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/OtherJob.java
new file mode 100644
index 0000000..b59ceae
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/OtherJob.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture.job;
+
+import io.elasticjob.cloud.api.ElasticJob;
+
+public final class OtherJob implements ElasticJob {
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/TestDataflowJob.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/TestDataflowJob.java
new file mode 100644
index 0000000..8e8d97f
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/TestDataflowJob.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture.job;
+
+import io.elasticjob.cloud.api.ShardingContext;
+import io.elasticjob.cloud.api.dataflow.DataflowJob;
+import lombok.RequiredArgsConstructor;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+public final class TestDataflowJob implements DataflowJob<Object> {
+
+ private final JobCaller jobCaller;
+
+ @Override
+ public List<Object> fetchData(final ShardingContext shardingContext) {
+ return jobCaller.fetchData(shardingContext.getShardingItem());
+ }
+
+ @Override
+ public void processData(final ShardingContext shardingContext, final List<Object> data) {
+ for (Object each : data) {
+ jobCaller.processData(each);
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/TestSimpleJob.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/TestSimpleJob.java
new file mode 100644
index 0000000..4ecc58e
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/TestSimpleJob.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture.job;
+
+import io.elasticjob.cloud.api.ShardingContext;
+import io.elasticjob.cloud.api.simple.SimpleJob;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public final class TestSimpleJob implements SimpleJob {
+
+ private final JobCaller jobCaller;
+
+ @Override
+ public void execute(final ShardingContext shardingContext) {
+ jobCaller.execute();
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/TestWrongJob.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/TestWrongJob.java
new file mode 100644
index 0000000..a631cf8
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/fixture/job/TestWrongJob.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.fixture.job;
+
+import io.elasticjob.cloud.api.ShardingContext;
+import io.elasticjob.cloud.api.simple.SimpleJob;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public final class TestWrongJob implements SimpleJob {
+
+ @Override
+ public void execute(final ShardingContext shardingContext) {
+ throw new RuntimeException("WrongJobException");
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/AllRegTests.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/AllRegTests.java
new file mode 100644
index 0000000..c68a685
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/AllRegTests.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg;
+
+import io.elasticjob.cloud.reg.zookeeper.ZookeeperRegistryCenterForAuthTest;
+import io.elasticjob.cloud.reg.zookeeper.ZookeeperRegistryCenterModifyTest;
+import io.elasticjob.cloud.reg.zookeeper.ZookeeperRegistryCenterQueryWithoutCacheTest;
+import io.elasticjob.cloud.reg.exception.RegExceptionHandlerTest;
+import io.elasticjob.cloud.reg.zookeeper.ZookeeperConfigurationTest;
+import io.elasticjob.cloud.reg.zookeeper.ZookeeperElectionServiceTest;
+import io.elasticjob.cloud.reg.zookeeper.ZookeeperRegistryCenterInitFailureTest;
+import io.elasticjob.cloud.reg.zookeeper.ZookeeperRegistryCenterMiscellaneousTest;
+import io.elasticjob.cloud.reg.zookeeper.ZookeeperRegistryCenterQueryWithCacheTest;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses({
+ ZookeeperConfigurationTest.class,
+ ZookeeperRegistryCenterForAuthTest.class,
+ ZookeeperRegistryCenterQueryWithCacheTest.class,
+ ZookeeperRegistryCenterQueryWithoutCacheTest.class,
+ ZookeeperRegistryCenterModifyTest.class,
+ ZookeeperRegistryCenterMiscellaneousTest.class,
+ ZookeeperElectionServiceTest.class,
+ RegExceptionHandlerTest.class,
+ ZookeeperRegistryCenterInitFailureTest.class
+ })
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class AllRegTests {
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/exception/RegExceptionHandlerTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/exception/RegExceptionHandlerTest.java
new file mode 100644
index 0000000..dec4358
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/exception/RegExceptionHandlerTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.exception;
+
+import org.apache.zookeeper.KeeperException.ConnectionLossException;
+import org.apache.zookeeper.KeeperException.NoNodeException;
+import org.apache.zookeeper.KeeperException.NodeExistsException;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public final class RegExceptionHandlerTest {
+
+ @Test
+ @Ignore
+ // TODO throw InterruptedException will cause zookeeper TestingServer break. Ignore first, fix it later.
+ public void assertHandleExceptionWithInterruptedException() {
+ RegExceptionHandler.handleException(new InterruptedException());
+ }
+
+ @Test
+ public void assertHandleExceptionWithNull() {
+ RegExceptionHandler.handleException(null);
+ }
+
+ @Test(expected = RegException.class)
+ public void assertHandleExceptionWithOtherException() {
+ RegExceptionHandler.handleException(new RuntimeException());
+ }
+
+ @Test
+ public void assertHandleExceptionWithConnectionLossException() {
+ RegExceptionHandler.handleException(new ConnectionLossException());
+ }
+
+ @Test
+ public void assertHandleExceptionWithNoNodeException() {
+ RegExceptionHandler.handleException(new NoNodeException());
+ }
+
+ @Test
+ public void assertHandleExceptionWithNoNodeExistsException() {
+ RegExceptionHandler.handleException(new NodeExistsException());
+ }
+
+ @Test
+ public void assertHandleExceptionWithCausedConnectionLossException() {
+ RegExceptionHandler.handleException(new RuntimeException(new ConnectionLossException()));
+ }
+
+ @Test
+ public void assertHandleExceptionWithCausedNoNodeException() {
+ RegExceptionHandler.handleException(new RuntimeException(new NoNodeException()));
+ }
+
+ @Test
+ public void assertHandleExceptionWithCausedNoNodeExistsException() {
+ RegExceptionHandler.handleException(new RuntimeException(new NodeExistsException()));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperConfigurationTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperConfigurationTest.java
new file mode 100644
index 0000000..b756d6f
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperConfigurationTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.zookeeper;
+
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public final class ZookeeperConfigurationTest {
+
+ @Test
+ public void assertNewZookeeperConfigurationForServerListsAndNamespace() {
+ ZookeeperConfiguration zkConfig = new ZookeeperConfiguration("localhost:2181", "myNamespace");
+ assertThat(zkConfig.getServerLists(), is("localhost:2181"));
+ assertThat(zkConfig.getNamespace(), is("myNamespace"));
+ assertThat(zkConfig.getBaseSleepTimeMilliseconds(), is(1000));
+ assertThat(zkConfig.getMaxSleepTimeMilliseconds(), is(3000));
+ assertThat(zkConfig.getMaxRetries(), is(3));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperElectionServiceTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperElectionServiceTest.java
new file mode 100644
index 0000000..bfe76d9
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperElectionServiceTest.java
@@ -0,0 +1,52 @@
+package io.elasticjob.cloud.reg.zookeeper;
+
+import io.elasticjob.cloud.fixture.EmbedTestingServer;
+import io.elasticjob.cloud.reg.base.ElectionCandidate;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.RetryOneTime;
+import org.apache.curator.test.KillSession;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ZookeeperElectionServiceTest {
+
+ private static final String HOST_AND_PORT = "localhost:8899";
+
+ private static final String ELECTION_PATH = "/election";
+
+ @Mock
+ private ElectionCandidate electionCandidate;
+
+ @BeforeClass
+ public static void init() throws InterruptedException {
+ EmbedTestingServer.start();
+ }
+
+ @Test
+ @Ignore
+ public void assertContend() throws Exception {
+ CuratorFramework client = CuratorFrameworkFactory.newClient(EmbedTestingServer.getConnectionString(), new RetryOneTime(2000));
+ client.start();
+ client.blockUntilConnected();
+ ZookeeperElectionService service = new ZookeeperElectionService(HOST_AND_PORT, client, ELECTION_PATH, electionCandidate);
+ service.start();
+ ElectionCandidate anotherElectionCandidate = mock(ElectionCandidate.class);
+ CuratorFramework anotherClient = CuratorFrameworkFactory.newClient(EmbedTestingServer.getConnectionString(), new RetryOneTime(2000));
+ ZookeeperElectionService anotherService = new ZookeeperElectionService("ANOTHER_CLIENT:8899", anotherClient, ELECTION_PATH, anotherElectionCandidate);
+ anotherClient.start();
+ anotherClient.blockUntilConnected();
+ anotherService.start();
+ KillSession.kill(client.getZookeeperClient().getZooKeeper(), EmbedTestingServer.getConnectionString());
+ service.stop();
+ verify(anotherElectionCandidate).startLeadership();
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterForAuthTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterForAuthTest.java
new file mode 100644
index 0000000..4449650
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterForAuthTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.zookeeper;
+
+import io.elasticjob.cloud.fixture.EmbedTestingServer;
+import io.elasticjob.cloud.reg.zookeeper.util.ZookeeperRegistryCenterTestUtil;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.RetryOneTime;
+import org.apache.zookeeper.KeeperException.NoAuthException;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+public final class ZookeeperRegistryCenterForAuthTest {
+
+ private static final String NAME_SPACE = ZookeeperRegistryCenterForAuthTest.class.getName();
+
+ private static final ZookeeperConfiguration ZOOKEEPER_CONFIGURATION = new ZookeeperConfiguration(EmbedTestingServer.getConnectionString(), NAME_SPACE);
+
+ private static ZookeeperRegistryCenter zkRegCenter;
+
+ @BeforeClass
+ public static void setUp() {
+ EmbedTestingServer.start();
+ ZOOKEEPER_CONFIGURATION.setDigest("digest:password");
+ ZOOKEEPER_CONFIGURATION.setSessionTimeoutMilliseconds(5000);
+ ZOOKEEPER_CONFIGURATION.setConnectionTimeoutMilliseconds(5000);
+ zkRegCenter = new ZookeeperRegistryCenter(ZOOKEEPER_CONFIGURATION);
+ zkRegCenter.init();
+ ZookeeperRegistryCenterTestUtil.persist(zkRegCenter);
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ zkRegCenter.close();
+ }
+
+ @Test
+ public void assertInitWithDigestSuccess() throws Exception {
+ CuratorFramework client = CuratorFrameworkFactory.builder()
+ .connectString(EmbedTestingServer.getConnectionString())
+ .retryPolicy(new RetryOneTime(2000))
+ .authorization("digest", "digest:password".getBytes()).build();
+ client.start();
+ client.blockUntilConnected();
+ assertThat(client.getData().forPath("/" + ZookeeperRegistryCenterForAuthTest.class.getName() + "/test/deep/nested"), is("deepNested".getBytes()));
+ }
+
+ @Test(expected = NoAuthException.class)
+ public void assertInitWithDigestFailure() throws Exception {
+ CuratorFramework client = CuratorFrameworkFactory.newClient(EmbedTestingServer.getConnectionString(), new RetryOneTime(2000));
+ client.start();
+ client.blockUntilConnected();
+ client.getData().forPath("/" + ZookeeperRegistryCenterForAuthTest.class.getName() + "/test/deep/nested");
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterInitFailureTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterInitFailureTest.java
new file mode 100644
index 0000000..891f757
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterInitFailureTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.zookeeper;
+
+import io.elasticjob.cloud.reg.exception.RegException;
+import org.junit.Test;
+
+public final class ZookeeperRegistryCenterInitFailureTest {
+
+ @Test(expected = RegException.class)
+ public void assertInitFailure() {
+ ZookeeperRegistryCenter zkRegCenter = new ZookeeperRegistryCenter(new ZookeeperConfiguration("localhost:1", ZookeeperRegistryCenterInitFailureTest.class.getName()));
+ zkRegCenter.init();
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterMiscellaneousTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterMiscellaneousTest.java
new file mode 100644
index 0000000..b5d32dc
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterMiscellaneousTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.zookeeper;
+
+import io.elasticjob.cloud.fixture.EmbedTestingServer;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.cache.TreeCache;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+public final class ZookeeperRegistryCenterMiscellaneousTest {
+
+ private static final ZookeeperConfiguration ZOOKEEPER_CONFIGURATION =
+ new ZookeeperConfiguration(EmbedTestingServer.getConnectionString(), ZookeeperRegistryCenterMiscellaneousTest.class.getName());
+
+ private static ZookeeperRegistryCenter zkRegCenter;
+
+ @BeforeClass
+ public static void setUp() {
+ EmbedTestingServer.start();
+ ZOOKEEPER_CONFIGURATION.setConnectionTimeoutMilliseconds(30000);
+ zkRegCenter = new ZookeeperRegistryCenter(ZOOKEEPER_CONFIGURATION);
+ zkRegCenter.init();
+ zkRegCenter.addCacheData("/test");
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ zkRegCenter.close();
+ }
+
+ @Test
+ public void assertGetRawClient() {
+ assertThat(zkRegCenter.getRawClient(), instanceOf(CuratorFramework.class));
+ assertThat(((CuratorFramework) zkRegCenter.getRawClient()).getNamespace(), is(ZookeeperRegistryCenterMiscellaneousTest.class.getName()));
+ }
+
+ @Test
+ public void assertGetRawCache() {
+ assertThat(zkRegCenter.getRawCache("/test"), instanceOf(TreeCache.class));
+ }
+
+ @Test
+ public void assertGetZkConfig() {
+ ZookeeperRegistryCenter zkRegCenter = new ZookeeperRegistryCenter(ZOOKEEPER_CONFIGURATION);
+ assertThat(zkRegCenter.getZkConfig(), is(ZOOKEEPER_CONFIGURATION));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterModifyTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterModifyTest.java
new file mode 100644
index 0000000..9d0f175
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterModifyTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.zookeeper;
+
+import io.elasticjob.cloud.fixture.EmbedTestingServer;
+import io.elasticjob.cloud.reg.zookeeper.util.ZookeeperRegistryCenterTestUtil;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.RetryOneTime;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public final class ZookeeperRegistryCenterModifyTest {
+
+ private static final ZookeeperConfiguration ZOOKEEPER_CONFIGURATION = new ZookeeperConfiguration(EmbedTestingServer.getConnectionString(), ZookeeperRegistryCenterModifyTest.class.getName());
+
+ private static ZookeeperRegistryCenter zkRegCenter;
+
+ @BeforeClass
+ public static void setUp() {
+ EmbedTestingServer.start();
+ zkRegCenter = new ZookeeperRegistryCenter(ZOOKEEPER_CONFIGURATION);
+ ZOOKEEPER_CONFIGURATION.setConnectionTimeoutMilliseconds(30000);
+ zkRegCenter.init();
+ ZookeeperRegistryCenterTestUtil.persist(zkRegCenter);
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ zkRegCenter.close();
+ }
+
+ @Test
+ public void assertPersist() {
+ zkRegCenter.persist("/test", "test_update");
+ zkRegCenter.persist("/persist/new", "new_value");
+ assertThat(zkRegCenter.get("/test"), is("test_update"));
+ assertThat(zkRegCenter.get("/persist/new"), is("new_value"));
+ }
+
+ @Test
+ public void assertUpdate() {
+ zkRegCenter.persist("/update", "before_update");
+ zkRegCenter.update("/update", "after_update");
+ assertThat(zkRegCenter.getDirectly("/update"), is("after_update"));
+ }
+
+ @Test
+ public void assertPersistEphemeral() throws Exception {
+ zkRegCenter.persist("/persist", "persist_value");
+ zkRegCenter.persistEphemeral("/ephemeral", "ephemeral_value");
+ assertThat(zkRegCenter.get("/persist"), is("persist_value"));
+ assertThat(zkRegCenter.get("/ephemeral"), is("ephemeral_value"));
+ zkRegCenter.close();
+ CuratorFramework client = CuratorFrameworkFactory.newClient(EmbedTestingServer.getConnectionString(), new RetryOneTime(2000));
+ client.start();
+ client.blockUntilConnected();
+ assertThat(client.getData().forPath("/" + ZookeeperRegistryCenterModifyTest.class.getName() + "/persist"), is("persist_value".getBytes()));
+ assertNull(client.checkExists().forPath("/" + ZookeeperRegistryCenterModifyTest.class.getName() + "/ephemeral"));
+ zkRegCenter.init();
+ }
+
+
+ @Test
+ public void assertPersistSequential() throws Exception {
+ assertThat(zkRegCenter.persistSequential("/sequential/test_sequential", "test_value"), startsWith("/sequential/test_sequential"));
+ assertThat(zkRegCenter.persistSequential("/sequential/test_sequential", "test_value"), startsWith("/sequential/test_sequential"));
+ CuratorFramework client = CuratorFrameworkFactory.newClient(EmbedTestingServer.getConnectionString(), new RetryOneTime(2000));
+ client.start();
+ client.blockUntilConnected();
+ List<String> actual = client.getChildren().forPath("/" + ZookeeperRegistryCenterModifyTest.class.getName() + "/sequential");
+ assertThat(actual.size(), is(2));
+ for (String each : actual) {
+ assertThat(each, startsWith("test_sequential"));
+ assertThat(zkRegCenter.get("/sequential/" + each), startsWith("test_value"));
+ }
+ }
+
+ @Test
+ public void assertPersistEphemeralSequential() throws Exception {
+ zkRegCenter.persistEphemeralSequential("/sequential/test_ephemeral_sequential");
+ zkRegCenter.persistEphemeralSequential("/sequential/test_ephemeral_sequential");
+ CuratorFramework client = CuratorFrameworkFactory.newClient(EmbedTestingServer.getConnectionString(), new RetryOneTime(2000));
+ client.start();
+ client.blockUntilConnected();
+ List<String> actual = client.getChildren().forPath("/" + ZookeeperRegistryCenterModifyTest.class.getName() + "/sequential");
+ assertThat(actual.size(), is(2));
+ for (String each : actual) {
+ assertThat(each, startsWith("test_ephemeral_sequential"));
+ }
+ zkRegCenter.close();
+ actual = client.getChildren().forPath("/" + ZookeeperRegistryCenterModifyTest.class.getName() + "/sequential");
+ assertTrue(actual.isEmpty());
+ zkRegCenter.init();
+ }
+
+ @Test
+ public void assertRemove() {
+ zkRegCenter.remove("/test");
+ assertFalse(zkRegCenter.isExisted("/test"));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterQueryWithCacheTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterQueryWithCacheTest.java
new file mode 100644
index 0000000..97c2143
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterQueryWithCacheTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.zookeeper;
+
+import io.elasticjob.cloud.fixture.EmbedTestingServer;
+import io.elasticjob.cloud.reg.zookeeper.util.ZookeeperRegistryCenterTestUtil;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+
+public final class ZookeeperRegistryCenterQueryWithCacheTest {
+
+ private static final ZookeeperConfiguration ZOOKEEPER_CONFIGURATION =
+ new ZookeeperConfiguration(EmbedTestingServer.getConnectionString(), ZookeeperRegistryCenterQueryWithCacheTest.class.getName());
+
+ private static ZookeeperRegistryCenter zkRegCenter;
+
+ @BeforeClass
+ public static void setUp() {
+ EmbedTestingServer.start();
+ zkRegCenter = new ZookeeperRegistryCenter(ZOOKEEPER_CONFIGURATION);
+ ZOOKEEPER_CONFIGURATION.setConnectionTimeoutMilliseconds(30000);
+ zkRegCenter.init();
+ ZookeeperRegistryCenterTestUtil.persist(zkRegCenter);
+ zkRegCenter.addCacheData("/test");
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ zkRegCenter.close();
+ }
+
+ @Test
+ public void assertGetWithoutValue() {
+ assertNull(zkRegCenter.get("/test/null"));
+ }
+
+ @Test
+ public void assertGetFromCache() {
+ assertThat(zkRegCenter.get("/test"), is("test"));
+ assertThat(zkRegCenter.get("/test/deep/nested"), is("deepNested"));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterQueryWithoutCacheTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterQueryWithoutCacheTest.java
new file mode 100644
index 0000000..3de97fb
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/ZookeeperRegistryCenterQueryWithoutCacheTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.reg.zookeeper;
+
+import io.elasticjob.cloud.fixture.EmbedTestingServer;
+import io.elasticjob.cloud.reg.zookeeper.util.ZookeeperRegistryCenterTestUtil;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static junit.framework.TestCase.assertNull;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public final class ZookeeperRegistryCenterQueryWithoutCacheTest {
+
+ private static final ZookeeperConfiguration ZOOKEEPER_CONFIGURATION =
+ new ZookeeperConfiguration(EmbedTestingServer.getConnectionString(), ZookeeperRegistryCenterQueryWithoutCacheTest.class.getName());
+
+ private static ZookeeperRegistryCenter zkRegCenter;
+
+ @BeforeClass
+ public static void setUp() {
+ EmbedTestingServer.start();
+ ZOOKEEPER_CONFIGURATION.setConnectionTimeoutMilliseconds(30000);
+ zkRegCenter = new ZookeeperRegistryCenter(ZOOKEEPER_CONFIGURATION);
+ zkRegCenter.init();
+ ZookeeperRegistryCenterTestUtil.persist(zkRegCenter);
+ zkRegCenter.addCacheData("/other");
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ zkRegCenter.close();
+ }
+
+ @Test
+ public void assertGetFromServer() {
+ assertThat(zkRegCenter.get("/test"), is("test"));
+ assertThat(zkRegCenter.get("/test/deep/nested"), is("deepNested"));
+ }
+
+ @Test
+ public void assertGetChildrenKeys() {
+ assertThat(zkRegCenter.getChildrenKeys("/test"), is(Arrays.asList("deep", "child")));
+ assertThat(zkRegCenter.getChildrenKeys("/test/deep"), is(Collections.singletonList("nested")));
+ assertThat(zkRegCenter.getChildrenKeys("/test/child"), is(Collections.<String>emptyList()));
+ assertThat(zkRegCenter.getChildrenKeys("/test/notExisted"), is(Collections.<String>emptyList()));
+ }
+
+ @Test
+ public void assertGetNumChildren() {
+ assertThat(zkRegCenter.getNumChildren("/test"), is(2));
+ assertThat(zkRegCenter.getNumChildren("/test/deep"), is(1));
+ assertThat(zkRegCenter.getNumChildren("/test/child"), is(0));
+ assertThat(zkRegCenter.getNumChildren("/test/notExisted"), is(0));
+ }
+
+ @Test
+ public void assertIsExisted() {
+ assertTrue(zkRegCenter.isExisted("/test"));
+ assertTrue(zkRegCenter.isExisted("/test/deep/nested"));
+ assertFalse(zkRegCenter.isExisted("/notExisted"));
+ }
+
+ @Test
+ public void assertGetRegistryCenterTime() {
+ long regCenterTime = zkRegCenter.getRegistryCenterTime("/_systemTime/current");
+ assertTrue(regCenterTime <= System.currentTimeMillis());
+ long updatedRegCenterTime = zkRegCenter.getRegistryCenterTime("/_systemTime/current");
+ System.out.println(regCenterTime + "," + updatedRegCenterTime);
+ assertTrue(regCenterTime < updatedRegCenterTime);
+ }
+
+ @Test
+ public void assertGetWithoutNode() {
+ assertNull(zkRegCenter.get("/notExisted"));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/util/ZookeeperRegistryCenterTestUtil.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/util/ZookeeperRegistryCenterTestUtil.java
new file mode 100644
index 0000000..485d94d
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/reg/zookeeper/util/ZookeeperRegistryCenterTestUtil.java
@@ -0,0 +1,15 @@
+package io.elasticjob.cloud.reg.zookeeper.util;
+
+import io.elasticjob.cloud.reg.zookeeper.ZookeeperRegistryCenter;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ZookeeperRegistryCenterTestUtil {
+
+ public static void persist(final ZookeeperRegistryCenter zookeeperRegistryCenter) {
+ zookeeperRegistryCenter.persist("/test", "test");
+ zookeeperRegistryCenter.persist("/test/deep/nested", "deepNested");
+ zookeeperRegistryCenter.persist("/test/child", "child");
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/statistics/AllStatisticsTests.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/statistics/AllStatisticsTests.java
new file mode 100644
index 0000000..0d1adc3
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/statistics/AllStatisticsTests.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.statistics;
+
+import io.elasticjob.cloud.statistics.rdb.StatisticRdbRepositoryTest;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses(StatisticRdbRepositoryTest.class)
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class AllStatisticsTests {
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/statistics/rdb/StatisticRdbRepositoryTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/statistics/rdb/StatisticRdbRepositoryTest.java
new file mode 100644
index 0000000..ae59806
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/statistics/rdb/StatisticRdbRepositoryTest.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.statistics.rdb;
+
+import com.google.common.base.Optional;
+import io.elasticjob.cloud.statistics.StatisticInterval;
+import io.elasticjob.cloud.statistics.type.job.JobRegisterStatistics;
+import io.elasticjob.cloud.statistics.type.job.JobRunningStatistics;
+import io.elasticjob.cloud.statistics.type.task.TaskResultStatistics;
+import io.elasticjob.cloud.statistics.type.task.TaskRunningStatistics;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.SQLException;
+import java.util.Date;
+
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+
+public class StatisticRdbRepositoryTest {
+
+ private StatisticRdbRepository repository;
+
+ @Before
+ public void setup() throws SQLException {
+ BasicDataSource dataSource = new BasicDataSource();
+ dataSource.setDriverClassName(org.h2.Driver.class.getName());
+ dataSource.setUrl("jdbc:h2:mem:");
+ dataSource.setUsername("sa");
+ dataSource.setPassword("");
+ repository = new StatisticRdbRepository(dataSource);
+ }
+
+ @Test
+ public void assertAddTaskResultStatistics() {
+ for (StatisticInterval each : StatisticInterval.values()) {
+ assertTrue(repository.add(new TaskResultStatistics(100, 0, each, new Date())));
+ }
+ }
+
+ @Test
+ public void assertAddTaskRunningStatistics() {
+ assertTrue(repository.add(new TaskRunningStatistics(100, new Date())));
+ }
+
+ @Test
+ public void assertAddJobRunningStatistics() {
+ assertTrue(repository.add(new TaskRunningStatistics(100, new Date())));
+ }
+
+ @Test
+ public void assertAddJobRegisterStatistics() {
+ assertTrue(repository.add(new JobRegisterStatistics(100, new Date())));
+ }
+
+ @Test
+ public void assertFindTaskResultStatisticsWhenTableIsEmpty() {
+ assertThat(repository.findTaskResultStatistics(new Date(), StatisticInterval.MINUTE).size(), is(0));
+ assertThat(repository.findTaskResultStatistics(new Date(), StatisticInterval.HOUR).size(), is(0));
+ assertThat(repository.findTaskResultStatistics(new Date(), StatisticInterval.DAY).size(), is(0));
+ }
+
+ @Test
+ public void assertFindTaskResultStatisticsWithDifferentFromDate() {
+ Date now = new Date();
+ Date yesterday = getYesterday();
+ for (StatisticInterval each : StatisticInterval.values()) {
+ assertTrue(repository.add(new TaskResultStatistics(100, 0, each, yesterday)));
+ assertTrue(repository.add(new TaskResultStatistics(100, 0, each, now)));
+ assertThat(repository.findTaskResultStatistics(yesterday, each).size(), is(2));
+ assertThat(repository.findTaskResultStatistics(now, each).size(), is(1));
+ }
+ }
+
+ @Test
+ public void assertGetSummedTaskResultStatisticsWhenTableIsEmpty() {
+ for (StatisticInterval each : StatisticInterval.values()) {
+ TaskResultStatistics po = repository.getSummedTaskResultStatistics(new Date(), each);
+ assertThat(po.getSuccessCount(), is(0));
+ assertThat(po.getFailedCount(), is(0));
+ }
+ }
+
+ @Test
+ public void assertGetSummedTaskResultStatistics() {
+ for (StatisticInterval each : StatisticInterval.values()) {
+ Date date = new Date();
+ repository.add(new TaskResultStatistics(100, 2, each, date));
+ repository.add(new TaskResultStatistics(200, 5, each, date));
+ TaskResultStatistics po = repository.getSummedTaskResultStatistics(date, each);
+ assertThat(po.getSuccessCount(), is(300));
+ assertThat(po.getFailedCount(), is(7));
+ }
+ }
+
+ @Test
+ public void assertFindLatestTaskResultStatisticsWhenTableIsEmpty() {
+ for (StatisticInterval each : StatisticInterval.values()) {
+ assertFalse(repository.findLatestTaskResultStatistics(each).isPresent());
+ }
+ }
+
+ @Test
+ public void assertFindLatestTaskResultStatistics() {
+ for (StatisticInterval each : StatisticInterval.values()) {
+ repository.add(new TaskResultStatistics(100, 2, each, new Date()));
+ repository.add(new TaskResultStatistics(200, 5, each, new Date()));
+ Optional<TaskResultStatistics> po = repository.findLatestTaskResultStatistics(each);
+ assertThat(po.get().getSuccessCount(), is(200));
+ assertThat(po.get().getFailedCount(), is(5));
+ }
+ }
+
+ @Test
+ public void assertFindTaskRunningStatisticsWhenTableIsEmpty() {
+ assertThat(repository.findTaskRunningStatistics(new Date()).size(), is(0));
+ }
+
+ @Test
+ public void assertFindTaskRunningStatisticsWithDifferentFromDate() {
+ Date now = new Date();
+ Date yesterday = getYesterday();
+ assertTrue(repository.add(new TaskRunningStatistics(100, yesterday)));
+ assertTrue(repository.add(new TaskRunningStatistics(100, now)));
+ assertThat(repository.findTaskRunningStatistics(yesterday).size(), is(2));
+ assertThat(repository.findTaskRunningStatistics(now).size(), is(1));
+ }
+
+ @Test
+ public void assertFindLatestTaskRunningStatisticsWhenTableIsEmpty() {
+ assertFalse(repository.findLatestTaskRunningStatistics().isPresent());
+ }
+
+ @Test
+ public void assertFindLatestTaskRunningStatistics() {
+ repository.add(new TaskRunningStatistics(100, new Date()));
+ repository.add(new TaskRunningStatistics(200, new Date()));
+ Optional<TaskRunningStatistics> po = repository.findLatestTaskRunningStatistics();
+ assertThat(po.get().getRunningCount(), is(200));
+ }
+
+ @Test
+ public void assertFindJobRunningStatisticsWhenTableIsEmpty() {
+ assertThat(repository.findJobRunningStatistics(new Date()).size(), is(0));
+ }
+
+ @Test
+ public void assertFindJobRunningStatisticsWithDifferentFromDate() {
+ Date now = new Date();
+ Date yesterday = getYesterday();
+ assertTrue(repository.add(new JobRunningStatistics(100, yesterday)));
+ assertTrue(repository.add(new JobRunningStatistics(100, now)));
+ assertThat(repository.findJobRunningStatistics(yesterday).size(), is(2));
+ assertThat(repository.findJobRunningStatistics(now).size(), is(1));
+ }
+
+ @Test
+ public void assertFindLatestJobRunningStatisticsWhenTableIsEmpty() {
+ assertFalse(repository.findLatestJobRunningStatistics().isPresent());
+ }
+
+ @Test
+ public void assertFindLatestJobRunningStatistics() {
+ repository.add(new JobRunningStatistics(100, new Date()));
+ repository.add(new JobRunningStatistics(200, new Date()));
+ Optional<JobRunningStatistics> po = repository.findLatestJobRunningStatistics();
+ assertThat(po.get().getRunningCount(), is(200));
+ }
+
+ @Test
+ public void assertFindJobRegisterStatisticsWhenTableIsEmpty() {
+ assertThat(repository.findJobRegisterStatistics(new Date()).size(), is(0));
+ }
+
+ @Test
+ public void assertFindJobRegisterStatisticsWithDifferentFromDate() {
+ Date now = new Date();
+ Date yesterday = getYesterday();
+ assertTrue(repository.add(new JobRegisterStatistics(100, yesterday)));
+ assertTrue(repository.add(new JobRegisterStatistics(100, now)));
+ assertThat(repository.findJobRegisterStatistics(yesterday).size(), is(2));
+ assertThat(repository.findJobRegisterStatistics(now).size(), is(1));
+ }
+
+ @Test
+ public void assertFindLatestJobRegisterStatisticsWhenTableIsEmpty() {
+ assertFalse(repository.findLatestJobRegisterStatistics().isPresent());
+ }
+
+ @Test
+ public void assertFindLatestJobRegisterStatistics() {
+ repository.add(new JobRegisterStatistics(100, new Date()));
+ repository.add(new JobRegisterStatistics(200, new Date()));
+ Optional<JobRegisterStatistics> po = repository.findLatestJobRegisterStatistics();
+ assertThat(po.get().getRegisteredCount(), is(200));
+ }
+
+ private Date getYesterday() {
+ return new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/AllUtilTests.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/AllUtilTests.java
new file mode 100644
index 0000000..092835a
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/AllUtilTests.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util;
+
+import io.elasticjob.cloud.util.concurrent.ExecutorServiceObjectTest;
+import io.elasticjob.cloud.util.config.ShardingItemParametersTest;
+import io.elasticjob.cloud.util.config.ShardingItemsTest;
+import io.elasticjob.cloud.util.digest.EncryptionTest;
+import io.elasticjob.cloud.util.env.HostExceptionTest;
+import io.elasticjob.cloud.util.env.IpUtilsTest;
+import io.elasticjob.cloud.util.env.TimeServiceTest;
+import io.elasticjob.cloud.util.json.GsonFactoryTest;
+import io.elasticjob.cloud.util.json.JobConfigurationGsonTypeAdapterTest;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses({
+ ExecutorServiceObjectTest.class,
+ EncryptionTest.class,
+ TimeServiceTest.class,
+ IpUtilsTest.class,
+ HostExceptionTest.class,
+ GsonFactoryTest.class,
+ JobConfigurationGsonTypeAdapterTest.class,
+ ShardingItemsTest.class,
+ ShardingItemParametersTest.class
+ })
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class AllUtilTests {
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/concurrent/ExecutorServiceObjectTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/concurrent/ExecutorServiceObjectTest.java
new file mode 100644
index 0000000..6c959c9
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/concurrent/ExecutorServiceObjectTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.concurrent;
+
+import org.junit.Test;
+
+import java.util.concurrent.ExecutorService;
+
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+
+public final class ExecutorServiceObjectTest {
+
+ private ExecutorServiceObject executorServiceObject;
+
+ @Test
+ public void assertCreateExecutorService() {
+ executorServiceObject = new ExecutorServiceObject("executor-service-test", 1);
+ assertThat(executorServiceObject.getActiveThreadCount(), is(0));
+ assertThat(executorServiceObject.getWorkQueueSize(), is(0));
+ assertFalse(executorServiceObject.isShutdown());
+ ExecutorService executorService = executorServiceObject.createExecutorService();
+ executorService.submit(new FooTask());
+ BlockUtils.waitingShortTime();
+ assertThat(executorServiceObject.getActiveThreadCount(), is(1));
+ assertThat(executorServiceObject.getWorkQueueSize(), is(0));
+ assertFalse(executorServiceObject.isShutdown());
+ executorService.submit(new FooTask());
+ BlockUtils.waitingShortTime();
+ assertThat(executorServiceObject.getActiveThreadCount(), is(1));
+ assertThat(executorServiceObject.getWorkQueueSize(), is(1));
+ assertFalse(executorServiceObject.isShutdown());
+ executorService.shutdownNow();
+ assertThat(executorServiceObject.getWorkQueueSize(), is(0));
+ assertTrue(executorServiceObject.isShutdown());
+ }
+
+ class FooTask implements Runnable {
+
+ @Override
+ public void run() {
+ BlockUtils.sleep(1000L);
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/config/ShardingItemParametersTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/config/ShardingItemParametersTest.java
new file mode 100644
index 0000000..8f7f3b0
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/config/ShardingItemParametersTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.config;
+
+import io.elasticjob.cloud.exception.JobConfigurationException;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+public final class ShardingItemParametersTest {
+
+ @Test(expected = JobConfigurationException.class)
+ public void assertNewWhenPairFormatInvalid() {
+ new ShardingItemParameters("xxx-xxx");
+ }
+
+ @Test(expected = JobConfigurationException.class)
+ public void assertNewWhenItemIsNotNumber() {
+ new ShardingItemParameters("xxx=xxx");
+ }
+
+ @Test
+ public void assertGetMapWhenIsEmpty() {
+ assertThat(new ShardingItemParameters("").getMap(), is(Collections.EMPTY_MAP));
+ }
+
+ @Test
+ public void assertGetMap() {
+ Map<Integer, String> expected = new HashMap<>(3);
+ expected.put(0, "A");
+ expected.put(1, "B");
+ expected.put(2, "C");
+ assertThat(new ShardingItemParameters("0=A,1=B,2=C").getMap(), is(expected));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/config/ShardingItemsTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/config/ShardingItemsTest.java
new file mode 100644
index 0000000..ebd61ad
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/config/ShardingItemsTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.config;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+public final class ShardingItemsTest {
+
+ @Test
+ public void assertTtoItemListWhenNull() {
+ assertThat(ShardingItems.toItemList(null), is(Collections.EMPTY_LIST));
+ }
+
+ @Test
+ public void assertToItemListWhenEmpty() {
+ assertThat(ShardingItems.toItemList(""), is(Collections.EMPTY_LIST));
+ }
+
+ @Test
+ public void assertToItemList() {
+ assertThat(ShardingItems.toItemList("0,1,2"), is(Arrays.asList(0, 1, 2)));
+ }
+
+ @Test
+ public void assertToItemListForDuplicated() {
+ assertThat(ShardingItems.toItemList("0,1,2,2"), is(Arrays.asList(0, 1, 2)));
+ }
+
+ @Test
+ public void assertToItemsStringWhenEmpty() {
+ assertThat(ShardingItems.toItemsString(Collections.<Integer>emptyList()), is(""));
+ }
+
+ @Test
+ public void assertToItemsString() {
+ assertThat(ShardingItems.toItemsString(Arrays.asList(0, 1, 2)), is("0,1,2"));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/digest/EncryptionTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/digest/EncryptionTest.java
new file mode 100644
index 0000000..c1ffe78
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/digest/EncryptionTest.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.digest;
+
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public final class EncryptionTest {
+
+ @Test
+ public void assertMd5() {
+ assertThat(Encryption.md5("test"), is("98f6bcd4621d373cade4e832627b4f6"));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/env/HostExceptionTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/env/HostExceptionTest.java
new file mode 100644
index 0000000..5424316
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/env/HostExceptionTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.env;
+
+import org.hamcrest.core.Is;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertThat;
+
+public final class HostExceptionTest {
+
+ @Test
+ public void assertGetCause() {
+ IOException cause = new IOException();
+ assertThat(new HostException(cause).getCause(), Is.<Throwable>is(cause));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/env/IpUtilsTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/env/IpUtilsTest.java
new file mode 100644
index 0000000..33d8144
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/env/IpUtilsTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.env;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+
+public final class IpUtilsTest {
+
+ @Test
+ public void assertGetIp() {
+ assertNotNull(IpUtils.getIp());
+ }
+
+ @Test
+ public void assertGetHostName() {
+ assertNotNull(IpUtils.getHostName());
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/env/TimeServiceTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/env/TimeServiceTest.java
new file mode 100644
index 0000000..1cca186
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/env/TimeServiceTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.env;
+
+import org.junit.Test;
+
+import static junit.framework.TestCase.assertTrue;
+
+
+public class TimeServiceTest {
+
+ private TimeService timeService = new TimeService();
+
+ @Test
+ public void assertGetCurrentMillis() throws Exception {
+ assertTrue(timeService.getCurrentMillis() <= System.currentTimeMillis());
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/json/GsonFactoryTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/json/GsonFactoryTest.java
new file mode 100644
index 0000000..0cfc991
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/json/GsonFactoryTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.json;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+public final class GsonFactoryTest {
+
+ @Test
+ public void assertGetGson() {
+ assertThat(GsonFactory.getGson(), is(GsonFactory.getGson()));
+ }
+
+ @Test
+ public void assertRegisterTypeAdapter() {
+ Gson beforeRegisterGson = GsonFactory.getGson();
+ GsonFactory.registerTypeAdapter(GsonFactoryTest.class, new TypeAdapter() {
+
+ @Override
+ public Object read(final JsonReader in) throws IOException {
+ return null;
+ }
+
+ @Override
+ public void write(final JsonWriter out, final Object value) throws IOException {
+ out.jsonValue("test");
+ }
+ });
+ assertThat(beforeRegisterGson.toJson(new GsonFactoryTest()), is("{}"));
+ assertThat(GsonFactory.getGson().toJson(new GsonFactoryTest()), is("test"));
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/json/JobConfigurationGsonTypeAdapterTest.java b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/json/JobConfigurationGsonTypeAdapterTest.java
new file mode 100644
index 0000000..ab92232
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/java/io/elasticjob/cloud/util/json/JobConfigurationGsonTypeAdapterTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.util.json;
+
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.elasticjob.cloud.config.JobTypeConfiguration;
+import io.elasticjob.cloud.fixture.APIJsonConstants;
+import io.elasticjob.cloud.fixture.config.TestDataflowJobConfiguration;
+import io.elasticjob.cloud.fixture.config.TestJobRootConfiguration;
+import io.elasticjob.cloud.fixture.config.TestScriptJobConfiguration;
+import io.elasticjob.cloud.fixture.config.TestSimpleJobConfiguration;
+import io.elasticjob.cloud.executor.handler.impl.DefaultExecutorServiceHandler;
+import io.elasticjob.cloud.fixture.handler.IgnoreJobExceptionHandler;
+import io.elasticjob.cloud.fixture.handler.ThrowJobExceptionHandler;
+import org.hamcrest.core.Is;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Map;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public final class JobConfigurationGsonTypeAdapterTest {
+
+ @BeforeClass
+ public static void setUp() {
+ GsonFactory.registerTypeAdapter(TestJobRootConfiguration.class, new JobConfigurationGsonTypeAdapter());
+ }
+
+ @Test
+ public void assertToSimpleJobJson() {
+ assertThat(GsonFactory.getGson().toJson(new TestJobRootConfiguration(
+ new TestSimpleJobConfiguration(ThrowJobExceptionHandler.class.getCanonicalName(), DefaultExecutorServiceHandler.class.getCanonicalName()).getTypeConfig())),
+ Is.is(APIJsonConstants.getSimpleJobJson(ThrowJobExceptionHandler.class.getCanonicalName())));
+ }
+
+ @Test
+ public void assertToDataflowJobJson() {
+ assertThat(GsonFactory.getGson().toJson(new TestJobRootConfiguration(new TestDataflowJobConfiguration(true).getTypeConfig())),
+ is(APIJsonConstants.getDataflowJobJson(IgnoreJobExceptionHandler.class.getCanonicalName())));
+ }
+
+ @Test
+ public void assertToScriptJobJson() {
+ assertThat(GsonFactory.getGson().toJson(new TestJobRootConfiguration(new TestScriptJobConfiguration("test.sh", ThrowJobExceptionHandler.class).getTypeConfig())),
+ is(APIJsonConstants.getScriptJobJson(ThrowJobExceptionHandler.class.getCanonicalName())));
+ }
+
+ @Test
+ public void assertFromSimpleJobJson() {
+ TestJobRootConfiguration actual = GsonFactory.getGson().fromJson(
+ APIJsonConstants.getSimpleJobJson(ThrowJobExceptionHandler.class.getCanonicalName()), TestJobRootConfiguration.class);
+ TestJobRootConfiguration expected = new TestJobRootConfiguration(
+ new TestSimpleJobConfiguration(ThrowJobExceptionHandler.class.getCanonicalName(), DefaultExecutorServiceHandler.class.getCanonicalName()).getTypeConfig());
+ assertThat(GsonFactory.getGson().toJson(actual), is(GsonFactory.getGson().toJson(expected)));
+ }
+
+ @Test
+ public void assertFromDataflowJobJson() {
+ TestJobRootConfiguration actual = GsonFactory.getGson().fromJson(
+ APIJsonConstants.getDataflowJobJson(IgnoreJobExceptionHandler.class.getCanonicalName()), TestJobRootConfiguration.class);
+ TestJobRootConfiguration expected = new TestJobRootConfiguration(new TestDataflowJobConfiguration(true).getTypeConfig());
+ assertThat(GsonFactory.getGson().toJson(actual), is(GsonFactory.getGson().toJson(expected)));
+ }
+
+ @Test
+ public void assertFromScriptJobJson() {
+ TestJobRootConfiguration actual = GsonFactory.getGson().fromJson(
+ APIJsonConstants.getScriptJobJson(ThrowJobExceptionHandler.class.getCanonicalName()), TestJobRootConfiguration.class);
+ TestJobRootConfiguration expected = new TestJobRootConfiguration(new TestScriptJobConfiguration("test.sh", ThrowJobExceptionHandler.class).getTypeConfig());
+ assertThat(GsonFactory.getGson().toJson(actual), is(GsonFactory.getGson().toJson(expected)));
+ }
+
+ private static class JobConfigurationGsonTypeAdapter extends AbstractJobConfigurationGsonTypeAdapter<TestJobRootConfiguration> {
+
+ @Override
+ protected void addToCustomizedValueMap(final String jsonName, final JsonReader in, final Map<String, Object> customizedValueMap) throws IOException {
+ }
+
+ @Override
+ protected TestJobRootConfiguration getJobRootConfiguration(final JobTypeConfiguration typeConfig, final Map<String, Object> customizedValueMap) {
+ return new TestJobRootConfiguration(typeConfig);
+ }
+
+ @Override
+ protected void writeCustomized(final JsonWriter out, final TestJobRootConfiguration value) throws IOException {
+ }
+ }
+}
diff --git a/elastic-job-cloud-common/src/test/resources/conf/reg/local.properties b/elastic-job-cloud-common/src/test/resources/conf/reg/local.properties
new file mode 100644
index 0000000..6b77fa2
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/resources/conf/reg/local.properties
@@ -0,0 +1,3 @@
+/test=test
+/test/deep/nested=deepNested
+/test/child=child
diff --git a/elastic-job-cloud-common/src/test/resources/conf/reg/local_overwrite.properties b/elastic-job-cloud-common/src/test/resources/conf/reg/local_overwrite.properties
new file mode 100644
index 0000000..41a69be
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/resources/conf/reg/local_overwrite.properties
@@ -0,0 +1,3 @@
+/test=test_overwrite
+/test/deep/nested=deepNested_overwrite
+/new=new
diff --git a/elastic-job-cloud-common/src/test/resources/logback-test.xml b/elastic-job-cloud-common/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..8db3a18
--- /dev/null
+++ b/elastic-job-cloud-common/src/test/resources/logback-test.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+ <property name="log.context.name" value="elastic-job-common-core-test" />
+ <property name="log.charset" value="UTF-8" />
+ <property name="log.pattern" value="[%-5level] %date --%thread-- [%logger] %msg %n" />
+
+ <contextName>${log.context.name}</contextName>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+ <level>ERROR</level>
+ </filter>
+ <encoder charset="${log.charset}">
+ <pattern>${log.pattern}</pattern>
+ </encoder>
+ </appender>
+
+ <root>
+ <appender-ref ref="STDOUT" />
+ </root>
+
+ <logger name="io.elasticjob.cloud.event.rdb.JobEventRdbConfiguration" level="OFF" />
+ <logger name="io.elasticjob.cloud.event.JobEventBus" level="OFF" />
+ <logger name="io.elasticjob.cloud.executor.handler.impl.DefaultJobExceptionHandler" level="OFF" />
+ <logger name="org.apache.curator.framework.recipes.leader.LeaderSelector" level="OFF" />
+</configuration>
diff --git a/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/restful/GsonJsonProvider.java b/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/restful/GsonJsonProvider.java
new file mode 100644
index 0000000..d52f68f
--- /dev/null
+++ b/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/restful/GsonJsonProvider.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.restful;
+
+import io.elasticjob.cloud.util.json.GsonFactory;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+/**
+ * 基于GSON解析JSON的解析器.
+ *
+ * @author zhangliang
+ */
+@Provider
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public final class GsonJsonProvider implements MessageBodyWriter<Object>, MessageBodyReader<Object> {
+
+ private static final String UTF_8 = "UTF-8";
+
+ @Override
+ public Object readFrom(final Class<Object> type, final Type genericType, final Annotation[] annotations,
+ final MediaType mediaType, final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) {
+ try (InputStreamReader streamReader = new InputStreamReader(entityStream, UTF_8)) {
+ return GsonFactory.getGson().fromJson(streamReader, type.equals(genericType) ? type : genericType);
+ } catch (final IOException ex) {
+ throw new RestfulException(ex);
+ }
+ }
+
+ @Override
+ public void writeTo(final Object object, final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType,
+ final MultivaluedMap<String, Object> httpHeaders, final OutputStream entityStream) throws IOException, WebApplicationException {
+ try (OutputStreamWriter writer = new OutputStreamWriter(entityStream, UTF_8)) {
+ GsonFactory.getGson().toJson(object, type.equals(genericType) ? type : genericType, writer);
+ } catch (final IOException ex) {
+ throw new RestfulException(ex);
+ }
+ }
+
+ @Override
+ public boolean isReadable(final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) {
+ return true;
+ }
+
+ @Override
+ public boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) {
+ return true;
+ }
+
+ @Override
+ public long getSize(final Object object, final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) {
+ return -1;
+ }
+}
diff --git a/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/restful/RestfulException.java b/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/restful/RestfulException.java
new file mode 100644
index 0000000..0768b73
--- /dev/null
+++ b/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/restful/RestfulException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.restful;
+
+/**
+ * REST API异常.
+ *
+ * @author zhangliang
+ */
+public final class RestfulException extends RuntimeException {
+
+ private static final long serialVersionUID = -7594937349408972960L;
+
+ public RestfulException(final Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/restful/RestfulExceptionMapper.java b/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/restful/RestfulExceptionMapper.java
new file mode 100644
index 0000000..6f5fc83
--- /dev/null
+++ b/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/restful/RestfulExceptionMapper.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.restful;
+
+import io.elasticjob.cloud.exception.ExceptionUtil;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * RESTFul API的异常处理器.
+ *
+ * @author zhangliang
+ */
+@Provider
+public final class RestfulExceptionMapper implements ExceptionMapper<Throwable> {
+
+ @Override
+ public Response toResponse(final Throwable cause) {
+ return Response.ok(ExceptionUtil.transform(cause), MediaType.TEXT_PLAIN).status(Response.Status.INTERNAL_SERVER_ERROR).build();
+ }
+}
diff --git a/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/restful/RestfulServer.java b/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/restful/RestfulServer.java
new file mode 100644
index 0000000..ae17748
--- /dev/null
+++ b/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/restful/RestfulServer.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.restful;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.sun.jersey.api.core.PackagesResourceConfig;
+import com.sun.jersey.spi.container.servlet.ServletContainer;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.resource.Resource;
+
+import javax.servlet.DispatcherType;
+import javax.servlet.Filter;
+import java.util.EnumSet;
+
+/**
+ * REST API的内嵌服务器.
+ *
+ * @author zhangliang
+ * @author caohao
+ */
+@Slf4j
+public final class RestfulServer {
+
+ private final Server server;
+
+ private final ServletContextHandler servletContextHandler;
+
+ public RestfulServer(final int port) {
+ server = new Server(port);
+ servletContextHandler = buildServletContextHandler();
+ }
+
+ /**
+ * 启动内嵌的RESTful服务器.
+ *
+ * @param packages RESTful实现类所在包
+ * @param resourcePath 资源路径
+ * @throws Exception 启动服务器异常
+ */
+ public void start(final String packages, final Optional<String> resourcePath) throws Exception {
+ start(packages, resourcePath, Optional.of("/api"));
+ }
+
+ /**
+ * 启动内嵌的RESTful服务器.
+ *
+ * @param packages RESTful实现类所在包
+ * @param resourcePath 资源路径
+ * @param servletPath servlet路径
+ * @throws Exception 启动服务器异常
+ */
+ public void start(final String packages, final Optional<String> resourcePath, final Optional<String> servletPath) throws Exception {
+ log.info("Elastic Job: Start RESTful server");
+ HandlerList handlers = new HandlerList();
+ if (resourcePath.isPresent()) {
+ servletContextHandler.setBaseResource(Resource.newClassPathResource(resourcePath.get()));
+ servletContextHandler.addServlet(new ServletHolder(DefaultServlet.class), "/*");
+ }
+ String servletPathStr = (servletPath.isPresent() ? servletPath.get() : "") + "/*";
+ servletContextHandler.addServlet(getServletHolder(packages), servletPathStr);
+ handlers.addHandler(servletContextHandler);
+ server.setHandler(handlers);
+ server.start();
+ }
+
+ /**
+ * 添加Filter.
+ *
+ * @param filterClass filter实现类
+ * @param urlPattern 过滤的路径
+ * @return RESTful服务器
+ */
+ public RestfulServer addFilter(final Class<? extends Filter> filterClass, final String urlPattern) {
+ servletContextHandler.addFilter(filterClass, urlPattern, EnumSet.of(DispatcherType.REQUEST));
+ return this;
+ }
+
+ private ServletContextHandler buildServletContextHandler() {
+ ServletContextHandler result = new ServletContextHandler(ServletContextHandler.SESSIONS);
+ result.setContextPath("/");
+ return result;
+ }
+
+ private ServletHolder getServletHolder(final String packages) {
+ ServletHolder result = new ServletHolder(ServletContainer.class);
+ result.setInitParameter(PackagesResourceConfig.PROPERTY_PACKAGES, Joiner.on(",").join(RestfulServer.class.getPackage().getName(), packages));
+ result.setInitParameter("com.sun.jersey.config.property.resourceConfigClass", PackagesResourceConfig.class.getName());
+ result.setInitParameter("com.sun.jersey.api.json.POJOMappingFeature", Boolean.TRUE.toString());
+ result.setInitParameter("resteasy.scan.providers", Boolean.TRUE.toString());
+ result.setInitParameter("resteasy.use.builtin.providers", Boolean.FALSE.toString());
+ return result;
+ }
+
+ /**
+ * 安静停止内嵌的RESTful服务器.
+ *
+ */
+ public void stop() {
+ log.info("Elastic Job: Stop RESTful server");
+ try {
+ server.stop();
+ // CHECKSTYLE:OFF
+ } catch (final Exception e) {
+ // CHECKSTYLE:ON
+ log.error("Elastic Job: Stop RESTful server error", e);
+ }
+ }
+}
diff --git a/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/security/WwwAuthFilter.java b/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/security/WwwAuthFilter.java
new file mode 100644
index 0000000..7a0d2c0
--- /dev/null
+++ b/elastic-job-cloud-scheduler/src/main/java/io/elasticjob/cloud/security/WwwAuthFilter.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.security;
+
+import com.google.common.base.Joiner;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Properties;
+
+/**
+ * 认证过滤器.
+ *
+ * @author zhangliang
+ */
+@Slf4j
+public final class WwwAuthFilter implements Filter {
+
+ private static final String FILE_SEPARATOR = System.getProperty("file.separator");
+
+ private static final String AUTH_PREFIX = "Basic ";
+
+ private static final String ROOT_IDENTIFY = "root";
+
+ private static final String ROOT_DEFAULT_USERNAME = "root";
+
+ private static final String ROOT_DEFAULT_PASSWORD = "root";
+
+ private static final String GUEST_IDENTIFY = "guest";
+
+ private static final String GUEST_DEFAULT_USERNAME = "guest";
+
+ private static final String GUEST_DEFAULT_PASSWORD = "guest";
+
+
+ private String rootUsername;
+
+ private String rootPassword;
+
+ private String guestUsername;
+
+ private String guestPassword;
+
+ @Override
+ public void init(final FilterConfig filterConfig) throws ServletException {
+ Properties props = new Properties();
+ URL classLoaderURL = Thread.currentThread().getContextClassLoader().getResource("");
+ if (null != classLoaderURL) {
+ String configFilePath = Joiner.on(FILE_SEPARATOR).join(classLoaderURL.getPath(), "conf", "auth.properties");
+ try {
+ props.load(new FileInputStream(configFilePath));
+ } catch (final IOException ex) {
+ log.warn("Cannot found auth config file, use default auth config.");
+ }
+ }
+ rootUsername = props.getProperty("root.username", ROOT_DEFAULT_USERNAME);
+ rootPassword = props.getProperty("root.password", ROOT_DEFAULT_PASSWORD);
+ guestUsername = props.getProperty("guest.username", GUEST_DEFAULT_USERNAME);
+ guestPassword = props.getProperty("guest.password", GUEST_DEFAULT_PASSWORD);
+ }
+
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ String authorization = httpRequest.getHeader("authorization");
+ if (null != authorization && authorization.length() > AUTH_PREFIX.length()) {
+ authorization = authorization.substring(AUTH_PREFIX.length(), authorization.length());
+ if ((rootUsername + ":" + rootPassword).equals(new String(Base64.decodeBase64(authorization)))) {
+ authenticateSuccess(httpResponse, false);
+ chain.doFilter(httpRequest, httpResponse);
+ } else if ((guestUsername + ":" + guestPassword).equals(new String(Base64.decodeBase64(authorization)))) {
+ authenticateSuccess(httpResponse, true);
+ chain.doFilter(httpRequest, httpResponse);
+ } else {
+ needAuthenticate(httpResponse);
+ }
+ } else {
+ needAuthenticate(httpResponse);
+ }
+ }
+
+ private void authenticateSuccess(final HttpServletResponse response, final boolean isGuest) {
+ response.setStatus(200);
+ response.setHeader("Pragma", "No-cache");
+ response.setHeader("Cache-Control", "no-store");
+ response.setDateHeader("Expires", 0);
+ response.setHeader("identify", isGuest ? GUEST_IDENTIFY : ROOT_IDENTIFY);
+ }
+
+ private void needAuthenticate(final HttpServletResponse response) {
+ response.setStatus(401);
+ response.setHeader("Cache-Control", "no-store");
+ response.setDateHeader("Expires", 0);
+ response.setHeader("WWW-authenticate", AUTH_PREFIX + "Realm=\"Elastic Job Console Auth\"");
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/AllTests.java b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/AllTests.java
new file mode 100644
index 0000000..f89ee0b
--- /dev/null
+++ b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/AllTests.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud;
+
+import io.elasticjob.cloud.restful.AllRestfulTests;
+import io.elasticjob.cloud.scheduler.AllCloudSchedulerTests;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses({
+ AllCloudSchedulerTests.class,
+ AllRestfulTests.class
+ })
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class AllTests {
+}
diff --git a/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/AllRestfulTests.java b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/AllRestfulTests.java
new file mode 100644
index 0000000..8bcef95
--- /dev/null
+++ b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/AllRestfulTests.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.restful;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses({
+ RestfulServerTest.class,
+ RestfulExceptionTest.class
+ })
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class AllRestfulTests {
+}
diff --git a/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/RestfulExceptionTest.java b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/RestfulExceptionTest.java
new file mode 100644
index 0000000..11bb883
--- /dev/null
+++ b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/RestfulExceptionTest.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.restful;
+
+import org.junit.Test;
+
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.assertThat;
+
+public final class RestfulExceptionTest {
+
+ @Test
+ public void assertRestfulException() {
+ assertThat(new RestfulException(new RuntimeException()).getCause(), instanceOf(RuntimeException.class));
+ }
+}
diff --git a/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/RestfulServerTest.java b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/RestfulServerTest.java
new file mode 100644
index 0000000..b8fe6e3
--- /dev/null
+++ b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/RestfulServerTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.restful;
+
+import com.google.common.base.Optional;
+import io.elasticjob.cloud.restful.fixture.TestFilter;
+import io.elasticjob.cloud.restful.fixture.TestRestfulApi;
+import io.elasticjob.cloud.restful.fixture.Caller;
+import org.eclipse.jetty.client.ContentExchange;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.io.ByteArrayBuffer;
+import org.hamcrest.core.Is;
+import org.hamcrest.core.StringStartsWith;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import javax.ws.rs.core.MediaType;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class RestfulServerTest {
+
+ private static final String URL = "http://127.0.0.1:17000/api/test/call";
+
+ private static RestfulServer server;
+
+ private Caller caller;
+
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ server = new RestfulServer(17000);
+ server.addFilter(TestFilter.class, "/*");
+ server.start(TestRestfulApi.class.getPackage().getName(), Optional.<String>absent());
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ server.stop();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ caller = Mockito.mock(Caller.class);
+ TestRestfulApi.setCaller(caller);
+ }
+
+ @Test
+ public void assertCallSuccess() throws Exception {
+ ContentExchange actual = sentRequest("{\"string\":\"test\",\"integer\":1}");
+ Assert.assertThat(actual.getResponseStatus(), Is.is(200));
+ Assert.assertThat(actual.getResponseContent(), Is.is("{\"string\":\"test_processed\",\"integer\":\"1_processed\"}"));
+ Mockito.verify(caller).call("test");
+ Mockito.verify(caller).call(1);
+ }
+
+ @Test
+ public void assertCallFailure() throws Exception {
+ ContentExchange actual = sentRequest("{\"string\":\"test\",\"integer\":\"invalid_number\"}");
+ Assert.assertThat(actual.getResponseStatus(), Is.is(500));
+ Assert.assertThat(actual.getResponseContent(), StringStartsWith.startsWith("java.lang.NumberFormatException"));
+ Mockito.verify(caller).call("test");
+ }
+
+ private static ContentExchange sentRequest(final String content) throws Exception {
+ HttpClient httpClient = new HttpClient();
+ try {
+ httpClient.start();
+ ContentExchange result = new ContentExchange();
+ result.setMethod("POST");
+ result.setRequestContentType(MediaType.APPLICATION_JSON);
+ result.setRequestContent(new ByteArrayBuffer(content.getBytes("UTF-8")));
+ httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
+ result.setURL(URL);
+ httpClient.send(result);
+ result.waitForDone();
+ return result;
+ } finally {
+ httpClient.stop();
+ }
+ }
+}
diff --git a/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/fixture/Caller.java b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/fixture/Caller.java
new file mode 100644
index 0000000..edee2d8
--- /dev/null
+++ b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/fixture/Caller.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.restful.fixture;
+
+public interface Caller {
+
+ void call(String value);
+
+ void call(int value);
+}
diff --git a/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/fixture/TestFilter.java b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/fixture/TestFilter.java
new file mode 100644
index 0000000..4f50044
--- /dev/null
+++ b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/fixture/TestFilter.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.restful.fixture;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public final class TestFilter implements Filter {
+
+ @Override
+ public void init(final FilterConfig filterConfig) throws ServletException {
+ }
+
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ chain.doFilter(httpRequest, httpResponse);
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/fixture/TestRestfulApi.java b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/fixture/TestRestfulApi.java
new file mode 100644
index 0000000..79390f9
--- /dev/null
+++ b/elastic-job-cloud-scheduler/src/test/java/io/elasticjob/cloud/restful/fixture/TestRestfulApi.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 1999-2015 dangdang.com.
+ * <p>
+ * 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.
+ * </p>
+ */
+
+package io.elasticjob.cloud.restful.fixture;
+
+import com.google.common.collect.Maps;
+import lombok.Setter;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.MediaType;
+import java.util.Map;
+
+@Path("/test")
+public final class TestRestfulApi {
+
+ @Setter
+ private static Caller caller;
+
+ @POST
+ @Path("/call")
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Map<String, String> call(final Map<String, String> map) {
+ caller.call(map.get("string"));
+ caller.call(Integer.valueOf(map.get("integer")));
+ return Maps.transformEntries(map, new Maps.EntryTransformer<String, String, String>() {
+
+ @Override
+ public String transformEntry(final String key, final String value) {
+ return value + "_processed";
+ }
+ });
+ }
+}