diff --git a/.github/workflows/e2e-k8s.yml b/.github/workflows/e2e-k8s.yml
index 3f93051c0cd5..a5964fb8fb0e 100644
--- a/.github/workflows/e2e-k8s.yml
+++ b/.github/workflows/e2e-k8s.yml
@@ -103,6 +103,92 @@ jobs:
name: shenyu-images
path: /tmp/apache-shenyu-*.tar
retention-days: 1
+
+ e2e-cluster:
+ runs-on: ubuntu-latest
+ needs:
+ - changes
+ - build-docker-images
+ if: (github.repository == 'apache/shenyu' && ${{ needs.changes.outputs.e2e == 'true' }})
+ strategy:
+ matrix:
+ include:
+ - case: shenyu-e2e-case-cluster
+ script: e2e-cluster-jdbc
+ - case: shenyu-e2e-case-cluster
+ script: e2e-cluster-zookeeper
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ submodules: true
+
+ - name: Free disk space
+ run: |
+ df --human-readable
+ sudo apt clean
+ docker rmi $(docker image ls --all --quiet)
+ rm --recursive --force "$AGENT_TOOLSDIRECTORY"
+ df --human-readable
+ rm -rf /tmp/shenyu
+ mkdir -p /tmp/shenyu
+
+ - uses: dorny/paths-filter@v2
+ id: filter
+ with:
+ filters: '.github/filters.yml'
+ list-files: json
+
+ - name: Install k8s
+ if: steps.filter.outputs.changed == 'true'
+ run: |
+ curl -sfL https://get.k3s.io | K3S_KUBECONFIG_MODE=777 sh -
+ cat /etc/rancher/k3s/k3s.yaml
+ mkdir -p ~/.kube
+ cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
+
+ - name: Set up JDK 17 for Building ShenYu
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: Restore ShenYu Maven Repos
+ if: steps.filter.outputs.changed == 'true'
+ uses: actions/cache/restore@v3
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - uses: actions/download-artifact@v3
+ with:
+ name: shenyu-images
+ path: /tmp/shenyu/
+
+ - name: Build k8s Cluster
+ if: steps.filter.outputs.changed == 'true'
+ run: |
+ sudo k3s ctr images import /tmp/shenyu/apache-shenyu-admin.tar
+ sudo k3s ctr images import /tmp/shenyu/apache-shenyu-bootstrap.tar
+
+ # - name: Setup Debug Session
+ # uses: mxschmitt/action-tmate@v3
+ # timeout-minutes: 15
+ # with:
+ # detached: true
+
+ - name: Run E2E Tests
+ if: steps.filter.outputs.changed == 'true'
+ run: |
+ bash ./shenyu-e2e/shenyu-e2e-case/${{ matrix.case }}/k8s/script/${{ matrix.script }}.sh
+
+ - name: Cluster Test after Healthcheck
+ if: steps.filter.outputs.changed == 'true'
+ run: |
+ kubectl get all
+ kubectl get events --all-namespaces
+ kubectl logs -l app=shenyu-admin-mysql
e2e-storage:
runs-on: ubuntu-latest
@@ -176,12 +262,6 @@ jobs:
sudo k3s ctr images import /tmp/shenyu/apache-shenyu-admin.tar
sudo k3s ctr images import /tmp/shenyu/apache-shenyu-bootstrap.tar
-# - name: Setup Debug Session
-# uses: mxschmitt/action-tmate@v3
-# timeout-minutes: 15
-# with:
-# detached: true
-
- name: Run E2E Tests
if: steps.filter.outputs.changed == 'true'
run: |
@@ -192,7 +272,6 @@ jobs:
run: |
kubectl get all
kubectl get events --all-namespaces
- kubectl logs -l app=shenyu-admin-mysql
e2e-case:
runs-on: ubuntu-latest
@@ -215,6 +294,7 @@ jobs:
script: e2e-grpc-sync
- case: shenyu-e2e-case-websocket
script: e2e-websocket-sync
+
steps:
- uses: actions/checkout@v2
with:
@@ -303,11 +383,13 @@ jobs:
if: ${{ needs.changes.outputs.e2e == 'true' }}
needs:
- changes
+ - e2e-cluster
- e2e-storage
- e2e-case
runs-on: ubuntu-latest
steps:
- name: checking job status
run: |
+ [[ "${{ needs.e2e-cluster.result }}" == "success" ]] || exit -1
[[ "${{ needs.e2e-storage.result }}" == "success" ]] || exit -1
[[ "${{ needs.e2e-case.result }}" == "success" ]] || exit -1
diff --git a/db/init/mysql/schema.sql b/db/init/mysql/schema.sql
index 8ddba4ef69b9..838e1d6b8720 100644
--- a/db/init/mysql/schema.sql
+++ b/db/init/mysql/schema.sql
@@ -2234,13 +2234,27 @@ CREATE TABLE IF NOT EXISTS `alert_receiver`
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
--- Table structure for INT_LOCK
+-- Table structure for sheny_lock
-- ----------------------------
-DROP TABLE IF EXISTS `INT_LOCK`;
-CREATE TABLE IF NOT EXISTS INT_LOCK (
+DROP TABLE IF EXISTS `SHENYU_LOCK`;
+CREATE TABLE IF NOT EXISTS SHENYU_LOCK (
`LOCK_KEY` CHAR(36) NOT NULL,
`REGION` VARCHAR(100) NOT NULL,
`CLIENT_ID` CHAR(36),
`CREATED_DATE` TIMESTAMP NOT NULL,
- constraint INT_LOCK_PK primary key (LOCK_KEY, REGION)
+ constraint SHENYU_LOCK_PK primary key (LOCK_KEY, REGION)
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for cluster_master
+-- ----------------------------
+DROP TABLE IF EXISTS `cluster_master`;
+CREATE TABLE IF NOT EXISTS cluster_master (
+ `id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'primary key id',
+ `master_host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'master host',
+ `master_port` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'master port',
+ `context_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'master context_path',
+ `date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'create time',
+ `date_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT 'update time',
+ PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
diff --git a/db/init/og/create-table.sql b/db/init/og/create-table.sql
index 34f0a1773f78..6038f2b8effc 100644
--- a/db/init/og/create-table.sql
+++ b/db/init/og/create-table.sql
@@ -2512,15 +2512,41 @@ COMMENT ON COLUMN "public"."alert_receiver"."type" IS 'notice type 0-SMS 1-Email
COMMENT ON COLUMN "public"."alert_receiver"."match_all" IS 'match all or not';
COMMENT ON COLUMN "public"."alert_receiver"."date_created" IS 'create time';
COMMENT ON COLUMN "public"."alert_receiver"."date_updated" IS 'update time';
-DROP TABLE IF EXISTS "public"."int_lock";
-CREATE TABLE "public"."int_lock" (
+
+-- ----------------------------
+-- Table structure for shenyu_lock
+-- ----------------------------
+DROP TABLE IF EXISTS "public"."shenyu_lock";
+CREATE TABLE "public"."shenyu_lock" (
"lock_key" CHAR(36) NOT NULL,
"region" VARCHAR(100) NOT NULL,
"client_id" CHAR(36),
"created_date" TIMESTAMP WITH TIME ZONE NOT NULL,
- CONSTRAINT INT_LOCK_PK PRIMARY KEY ("lock_key", "region")
+ CONSTRAINT shenyu_lock_pk PRIMARY KEY ("lock_key", "region")
);
-COMMENT ON COLUMN "public"."int_lock"."lock_key" IS 'lock_key';
-COMMENT ON COLUMN "public"."int_lock"."region" IS 'region';
-COMMENT ON COLUMN "public"."int_lock"."client_id" IS 'client_id';
-COMMENT ON COLUMN "public"."int_lock"."created_date" IS 'created_date';
+COMMENT ON COLUMN "public"."shenyu_lock"."lock_key" IS 'lock_key';
+COMMENT ON COLUMN "public"."shenyu_lock"."region" IS 'region';
+COMMENT ON COLUMN "public"."shenyu_lock"."client_id" IS 'client_id';
+COMMENT ON COLUMN "public"."shenyu_lock"."created_date" IS 'created_date';
+
+
+-- ----------------------------
+-- Table structure for cluster_master
+-- ----------------------------
+DROP TABLE IF EXISTS "public"."cluster_master";
+CREATE TABLE "public"."cluster_master"
+(
+ "id" varchar(128) COLLATE "pg_catalog"."default" NOT NULL PRIMARY KEY,
+ "master_host" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
+ "master_port" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
+ "context_path" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
+ "date_created" timestamp(6) NOT NULL DEFAULT timezone('UTC-8'::text, (now())::timestamp(0) without time zone),
+ "date_updated" timestamp(6) NOT NULL DEFAULT timezone('UTC-8'::text, (now())::timestamp(0) without time zone)
+)
+;
+COMMENT ON COLUMN "public"."cluster_master"."id" IS 'primary key id';
+COMMENT ON COLUMN "public"."cluster_master"."master_host" IS 'master host';
+COMMENT ON COLUMN "public"."cluster_master"."master_port" IS 'master port';
+COMMENT ON COLUMN "public"."cluster_master"."context_path" IS 'master context_path';
+COMMENT ON COLUMN "public"."cluster_master"."date_created" IS 'create time';
+COMMENT ON COLUMN "public"."cluster_master"."date_updated" IS 'update time';
\ No newline at end of file
diff --git a/db/init/oracle/schema.sql b/db/init/oracle/schema.sql
index d76eabe101be..37cb57e30bda 100644
--- a/db/init/oracle/schema.sql
+++ b/db/init/oracle/schema.sql
@@ -2680,19 +2680,58 @@ comment
on column alert_receiver.date_updated
is 'update time';
-CREATE TABLE INT_LOCK (
+
+-- ----------------------------
+-- Table structure for SHENYU_LOCK
+-- ----------------------------
+CREATE TABLE SHENYU_LOCK (
LOCK_KEY CHAR(36),
REGION VARCHAR(100),
CLIENT_ID CHAR(36),
CREATED_DATE TIMESTAMP NOT NULL,
- constraint INT_LOCK_PK primary key (LOCK_KEY, REGION)
+ constraint SHENYU_LOCK_PK primary key (LOCK_KEY, REGION)
);
-- Add comments to the columns
-comment on column INT_LOCK.LOCK_KEY
+comment on column SHENYU_LOCK.LOCK_KEY
is 'LOCK_KEY';
-comment on column INT_LOCK.REGION
+comment on column SHENYU_LOCK.REGION
is 'REGION';
-comment on column INT_LOCK.CLIENT_ID
+comment on column SHENYU_LOCK.CLIENT_ID
is 'CLIENT_ID';
-comment on column INT_LOCK.CREATED_DATE
+comment on column SHENYU_LOCK.CREATED_DATE
is 'CREATED_DATE';
+
+-- ----------------------------
+-- Table structure for cluster_master
+-- ----------------------------
+create table cluster_master
+(
+ id varchar(128) not null,
+ master_host varchar(255) not null,
+ master_port varchar(255) not null,
+ context_path varchar(255) not null,
+ date_created timestamp(3) default SYSDATE not null,
+ date_updated timestamp(3) default SYSDATE not null,
+ PRIMARY KEY (id)
+)
+;
+-- Add comments to the columns
+comment
+on column alert_receiver.id
+ is 'primary key id';
+comment
+on column alert_receiver.master_host
+ is 'master host';
+comment
+on column alert_receiver.master_port
+ is 'master port';
+comment
+on column alert_receiver.context_path
+ is 'master context_path';
+comment
+on column alert_receiver.date_created
+ is 'create time';
+comment
+on column alert_receiver.date_updated
+ is 'update time';
+
diff --git a/db/init/pg/create-table.sql b/db/init/pg/create-table.sql
index 5ec5ec4b6b43..dab064fd5e90 100644
--- a/db/init/pg/create-table.sql
+++ b/db/init/pg/create-table.sql
@@ -2635,15 +2635,39 @@ COMMENT ON COLUMN "public"."alert_receiver"."match_all" IS 'match all or not';
COMMENT ON COLUMN "public"."alert_receiver"."date_created" IS 'create time';
COMMENT ON COLUMN "public"."alert_receiver"."date_updated" IS 'update time';
-DROP TABLE IF EXISTS "public"."int_lock";
-CREATE TABLE "public"."int_lock" (
+-- ----------------------------
+-- Table structure for shenyu_lock
+-- ----------------------------
+DROP TABLE IF EXISTS "public"."shenyu_lock";
+CREATE TABLE "public"."shenyu_lock" (
"lock_key" CHAR(36) NOT NULL,
"region" VARCHAR(100) NOT NULL,
"client_id" CHAR(36),
"created_date" TIMESTAMP WITH TIME ZONE NOT NULL,
- CONSTRAINT INT_LOCK_PK PRIMARY KEY ("lock_key", "region")
+ CONSTRAINT shenyu_lock_pk PRIMARY KEY ("lock_key", "region")
);
-COMMENT ON COLUMN "public"."int_lock"."lock_key" IS 'lock_key';
-COMMENT ON COLUMN "public"."int_lock"."region" IS 'region';
-COMMENT ON COLUMN "public"."int_lock"."client_id" IS 'client_id';
-COMMENT ON COLUMN "public"."int_lock"."created_date" IS 'created_date';
+COMMENT ON COLUMN "public"."shenyu_lock"."lock_key" IS 'lock_key';
+COMMENT ON COLUMN "public"."shenyu_lock"."region" IS 'region';
+COMMENT ON COLUMN "public"."shenyu_lock"."client_id" IS 'client_id';
+COMMENT ON COLUMN "public"."shenyu_lock"."created_date" IS 'created_date';
+
+-- ----------------------------
+-- Table structure for cluster_master
+-- ----------------------------
+DROP TABLE IF EXISTS "public"."cluster_master";
+CREATE TABLE "public"."cluster_master"
+(
+ "id" varchar(128) COLLATE "pg_catalog"."default" NOT NULL PRIMARY KEY,
+ "master_host" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
+ "master_port" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
+ "context_path" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
+ "date_created" timestamp(6) NOT NULL DEFAULT timezone('UTC-8'::text, (now())::timestamp(0) without time zone),
+ "date_updated" timestamp(6) NOT NULL DEFAULT timezone('UTC-8'::text, (now())::timestamp(0) without time zone)
+)
+;
+COMMENT ON COLUMN "public"."cluster_master"."id" IS 'primary key id';
+COMMENT ON COLUMN "public"."cluster_master"."master_host" IS 'master host';
+COMMENT ON COLUMN "public"."cluster_master"."master_port" IS 'master port';
+COMMENT ON COLUMN "public"."cluster_master"."context_path" IS 'master context_path';
+COMMENT ON COLUMN "public"."cluster_master"."date_created" IS 'create time';
+COMMENT ON COLUMN "public"."cluster_master"."date_updated" IS 'update time';
\ No newline at end of file
diff --git a/db/upgrade/2.6.1-upgrade-2.7.0-mysql.sql b/db/upgrade/2.6.1-upgrade-2.7.0-mysql.sql
index 6650f6082240..b5d820f65e3f 100755
--- a/db/upgrade/2.6.1-upgrade-2.7.0-mysql.sql
+++ b/db/upgrade/2.6.1-upgrade-2.7.0-mysql.sql
@@ -21,16 +21,17 @@ INSERT INTO `plugin_handle` VALUES ('1722804548510507023', '3', 'rewriteMetaData
INSERT INTO `shenyu_dict` VALUES ('1679002911061737478', 'rewriteMetaData', 'REWRITE_META_DATA', 'true', 'true', '', 4, 1, '2024-02-07 14:31:49', '2024-02-07 14:31:49');
INSERT INTO `shenyu_dict` VALUES ('1679002911061737479', 'rewriteMetaData', 'REWRITE_META_DATA', 'false', 'false', '', 4, 1, '2024-02-07 14:31:49', '2024-02-07 14:31:49');
+
-- ----------------------------
--- Table structure for INT_LOCK
+-- Table structure for sheny_lock
-- ----------------------------
-DROP TABLE IF EXISTS `INT_LOCK`;
-CREATE TABLE IF NOT EXISTS INT_LOCK (
+DROP TABLE IF EXISTS `SHENYU_LOCK`;
+CREATE TABLE IF NOT EXISTS SHENYU_LOCK (
`LOCK_KEY` CHAR(36) NOT NULL,
`REGION` VARCHAR(100) NOT NULL,
`CLIENT_ID` CHAR(36),
`CREATED_DATE` TIMESTAMP NOT NULL,
- constraint INT_LOCK_PK primary key (LOCK_KEY, REGION)
+ constraint SHENYU_LOCK_PK primary key (LOCK_KEY, REGION)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
INSERT INTO `resource` VALUES ('1347048240677269503', '1346777766301888512', 'SHENYU.PLUGIN.BATCH.OPENED', '', '', '', 2, 3, '', 1, 0, 'system:authen:open', 1, '2022-05-25 18:02:53', '2022-05-25 18:02:53');
@@ -40,3 +41,18 @@ INSERT INTO `resource` VALUES ('1386680049203195915', '1346777157943259136', 'SH
INSERT INTO `resource` VALUES ('1386680049203195916', '1346777157943259136', 'SHENYU.COMMON.IMPORT', '', '', '', 2, 0, '', 1, 0, 'system:manager:importConfig', 1, '2022-05-25 18:02:53', '2022-05-25 18:02:53');
INSERT INTO `permission` VALUES ('1386680049203195906', '1346358560427216896', '1386680049203195915', '2022-05-25 18:02:53', '2022-05-25 18:02:53');
INSERT INTO `permission` VALUES ('1386680049203195907', '1346358560427216896', '1386680049203195916', '2022-05-25 18:02:53', '2022-05-25 18:02:53');
+
+
+-- ----------------------------
+-- Table structure for cluster_master
+-- ----------------------------
+DROP TABLE IF EXISTS `cluster_master`;
+CREATE TABLE IF NOT EXISTS cluster_master (
+ `id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'primary key id',
+ `master_host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'master host',
+ `master_port` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'master port',
+ `context_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'master context_path',
+ `date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'create time',
+ `date_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT 'update time',
+ PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
\ No newline at end of file
diff --git a/db/upgrade/2.6.1-upgrade-2.7.0-og.sql b/db/upgrade/2.6.1-upgrade-2.7.0-og.sql
index 1f3f3afaa4fa..b19fa29ac3d2 100644
--- a/db/upgrade/2.6.1-upgrade-2.7.0-og.sql
+++ b/db/upgrade/2.6.1-upgrade-2.7.0-og.sql
@@ -22,18 +22,22 @@ INSERT INTO "public"."plugin_handle" VALUES ('1722804548510507022', '3', 'rewrit
INSERT INTO "public"."shenyu_dict" VALUES ('1679002911061737478', 'rewriteMetaData', 'REWRITE_META_DATA', 'true', 'true', '', 4, 1, '2024-02-07 14:31:49', '2024-02-07 14:31:49');
INSERT INTO "public"."shenyu_dict" VALUES ('1679002911061737479', 'rewriteMetaData', 'REWRITE_META_DATA', 'false', 'false', '', 4, 1, '2024-02-07 14:31:49', '2024-02-07 14:31:49');
-DROP TABLE IF EXISTS "public"."int_lock";
-CREATE TABLE "public"."int_lock" (
+-- ----------------------------
+-- Table structure for shenyu_lock
+-- ----------------------------
+DROP TABLE IF EXISTS "public"."shenyu_lock";
+CREATE TABLE "public"."shenyu_lock" (
"lock_key" CHAR(36) NOT NULL,
"region" VARCHAR(100) NOT NULL,
"client_id" CHAR(36),
"created_date" TIMESTAMP WITH TIME ZONE NOT NULL,
- CONSTRAINT INT_LOCK_PK PRIMARY KEY ("lock_key", "region")
+ CONSTRAINT shenyu_lock_pk PRIMARY KEY ("lock_key", "region")
);
-COMMENT ON COLUMN "public"."int_lock"."lock_key" IS 'lock_key';
-COMMENT ON COLUMN "public"."int_lock"."region" IS 'region';
-COMMENT ON COLUMN "public"."int_lock"."client_id" IS 'client_id';
-COMMENT ON COLUMN "public"."int_lock"."created_date" IS 'created_date';
+COMMENT ON COLUMN "public"."shenyu_lock"."lock_key" IS 'lock_key';
+COMMENT ON COLUMN "public"."shenyu_lock"."region" IS 'region';
+COMMENT ON COLUMN "public"."shenyu_lock"."client_id" IS 'client_id';
+COMMENT ON COLUMN "public"."shenyu_lock"."created_date" IS 'created_date';
+
INSERT INTO "public"."resource" VALUES ('1347048240677269503', '1346777766301888512', 'SHENYU.PLUGIN.BATCH.OPENED', '', '', '', 2, 3, '', 1, 0, 'system:authen:open', 1, '2022-05-25 18:08:01', '2022-05-25 18:08:01');
INSERT INTO "public"."permission" VALUES ('1351007708748849151', '1346358560427216896', '1347048240677269503', '2022-05-25 18:08:01', '2022-05-25 18:08:01');
@@ -42,3 +46,25 @@ INSERT INTO "public"."resource" VALUES ('1386680049203195915', '1346777157943259
INSERT INTO "public"."resource" VALUES ('1386680049203195916', '1346777157943259136', 'SHENYU.COMMON.IMPORT', '', '', '', 2, 0, '', 1, 0, 'system:manager:importConfig', 1, '2022-05-25 18:08:01', '2022-05-25 18:08:01');
INSERT INTO "public"."permission" VALUES ('1386680049203195906', '1346358560427216896', '1386680049203195915', '2022-05-25 18:08:01', '2022-05-25 18:08:01');
INSERT INTO "public"."permission" VALUES ('1386680049203195907', '1346358560427216896', '1386680049203195916', '2022-05-25 18:08:01', '2022-05-25 18:08:01');
+
+
+-- ----------------------------
+-- Table structure for cluster_master
+-- ----------------------------
+DROP TABLE IF EXISTS "public"."cluster_master";
+CREATE TABLE "public"."cluster_master"
+(
+ "id" varchar(128) COLLATE "pg_catalog"."default" NOT NULL PRIMARY KEY,
+ "master_host" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
+ "master_port" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
+ "context_path" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
+ "date_created" timestamp(6) NOT NULL DEFAULT timezone('UTC-8'::text, (now())::timestamp(0) without time zone),
+ "date_updated" timestamp(6) NOT NULL DEFAULT timezone('UTC-8'::text, (now())::timestamp(0) without time zone)
+)
+;
+COMMENT ON COLUMN "public"."cluster_master"."id" IS 'primary key id';
+COMMENT ON COLUMN "public"."cluster_master"."master_host" IS 'master host';
+COMMENT ON COLUMN "public"."cluster_master"."master_port" IS 'master port';
+COMMENT ON COLUMN "public"."cluster_master"."context_path" IS 'master context_path';
+COMMENT ON COLUMN "public"."cluster_master"."date_created" IS 'create time';
+COMMENT ON COLUMN "public"."cluster_master"."date_updated" IS 'update time';
\ No newline at end of file
diff --git a/db/upgrade/2.6.1-upgrade-2.7.0-oracle.sql b/db/upgrade/2.6.1-upgrade-2.7.0-oracle.sql
index f552a86000db..b98b9bf343b5 100755
--- a/db/upgrade/2.6.1-upgrade-2.7.0-oracle.sql
+++ b/db/upgrade/2.6.1-upgrade-2.7.0-oracle.sql
@@ -30,23 +30,27 @@ values ('1722804548510507021', '14', 'percentage', 'percentage', 1, 2, 3, '{"req
insert /*+ IGNORE_ROW_ON_DUPKEY_INDEX(plugin_handle(plugin_id, field, type)) */ into plugin_handle (ID, PLUGIN_ID, FIELD, LABEL, DATA_TYPE, TYPE, SORT, EXT_OBJ)
values ('1722804548510507022', '3', 'rewriteMetaData', 'rewriteMetaData', 3, 2, 3, '{"required":"1","defaultValue":"false"}');
-CREATE TABLE INT_LOCK (
- LOCK_KEY CHAR(36),
- REGION VARCHAR(100),
- CLIENT_ID CHAR(36),
- CREATED_DATE TIMESTAMP NOT NULL,
- constraint INT_LOCK_PK primary key (LOCK_KEY, REGION)
+-- ----------------------------
+-- Table structure for SHENYU_LOCK
+-- ----------------------------
+CREATE TABLE SHENYU_LOCK (
+ LOCK_KEY CHAR(36),
+ REGION VARCHAR(100),
+ CLIENT_ID CHAR(36),
+ CREATED_DATE TIMESTAMP NOT NULL,
+ constraint SHENYU_LOCK_PK primary key (LOCK_KEY, REGION)
);
-- Add comments to the columns
-comment on column INT_LOCK.LOCK_KEY
+comment on column SHENYU_LOCK.LOCK_KEY
is 'LOCK_KEY';
-comment on column INT_LOCK.REGION
+comment on column SHENYU_LOCK.REGION
is 'REGION';
-comment on column INT_LOCK.CLIENT_ID
+comment on column SHENYU_LOCK.CLIENT_ID
is 'CLIENT_ID';
-comment on column INT_LOCK.CREATED_DATE
+comment on column SHENYU_LOCK.CREATED_DATE
is 'CREATED_DATE';
+
INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX("resource" (id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status) VALUES('1347048240677269503','1346777766301888512','SHENYU.PLUGIN.BATCH.OPENED','','','','2','3','','1','0','system:authen:open','1');
INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX (permission(id)) */ INTO permission (id, object_id, resource_id) VALUES ('1351007708748849151', '1346358560427216896', '1347048240677269503');
@@ -54,3 +58,38 @@ INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX("resource" (id)) */ INTO "resource" (id,
INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX("resource" (id)) */ INTO "resource" (id, parent_id, title, name, url, component, resource_type, sort, icon, is_leaf, is_route, perms, status) VALUES('1386680049203195916','1346777157943259136','SHENYU.COMMON.IMPORT', '', '', '', 2, 0, '', 1, 0, 'system:manager:importConfig', 1);
INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX (permission(id)) */ INTO permission (id, object_id, resource_id) VALUES ('1386680049203195906', '1346358560427216896', '1386680049203195915');
INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX (permission(id)) */ INTO permission (id, object_id, resource_id) VALUES ('1386680049203195907', '1346358560427216896', '1386680049203195916');
+
+
+-- ----------------------------
+-- Table structure for cluster_master
+-- ----------------------------
+create table cluster_master
+(
+ id varchar(128) not null,
+ master_host varchar(255) not null,
+ master_port varchar(255) not null,
+ context_path varchar(255) not null,
+ date_created timestamp(3) default SYSDATE not null,
+ date_updated timestamp(3) default SYSDATE not null,
+ PRIMARY KEY (id)
+)
+;
+-- Add comments to the columns
+comment
+on column alert_receiver.id
+ is 'primary key id';
+comment
+on column alert_receiver.master_host
+ is 'master host';
+comment
+on column alert_receiver.master_port
+ is 'master port';
+comment
+on column alert_receiver.context_path
+ is 'master context_path';
+comment
+on column alert_receiver.date_created
+ is 'create time';
+comment
+on column alert_receiver.date_updated
+ is 'update time';
\ No newline at end of file
diff --git a/db/upgrade/2.6.1-upgrade-2.7.0-pg.sql b/db/upgrade/2.6.1-upgrade-2.7.0-pg.sql
index 993affbd6ba5..07abcc945987 100755
--- a/db/upgrade/2.6.1-upgrade-2.7.0-pg.sql
+++ b/db/upgrade/2.6.1-upgrade-2.7.0-pg.sql
@@ -22,18 +22,22 @@ INSERT INTO "public"."plugin_handle" VALUES ('1722804548510507022', '3', 'rewrit
INSERT INTO "public"."shenyu_dict" VALUES ('1679002911061737478', 'rewriteMetaData', 'REWRITE_META_DATA', 'true', 'true', '', 4, 1, '2024-02-07 14:31:49', '2024-02-07 14:31:49');
INSERT INTO "public"."shenyu_dict" VALUES ('1679002911061737479', 'rewriteMetaData', 'REWRITE_META_DATA', 'false', 'false', '', 4, 1, '2024-02-07 14:31:49', '2024-02-07 14:31:49');
-DROP TABLE IF EXISTS "public"."int_lock";
-CREATE TABLE "public"."int_lock" (
+-- ----------------------------
+-- Table structure for shenyu_lock
+-- ----------------------------
+DROP TABLE IF EXISTS "public"."shenyu_lock";
+CREATE TABLE "public"."shenyu_lock" (
"lock_key" CHAR(36) NOT NULL,
"region" VARCHAR(100) NOT NULL,
"client_id" CHAR(36),
"created_date" TIMESTAMP WITH TIME ZONE NOT NULL,
- CONSTRAINT INT_LOCK_PK PRIMARY KEY ("lock_key", "region")
+ CONSTRAINT shenyu_lock_pk PRIMARY KEY ("lock_key", "region")
);
-COMMENT ON COLUMN "public"."int_lock"."lock_key" IS 'lock_key';
-COMMENT ON COLUMN "public"."int_lock"."region" IS 'region';
-COMMENT ON COLUMN "public"."int_lock"."client_id" IS 'client_id';
-COMMENT ON COLUMN "public"."int_lock"."created_date" IS 'created_date';
+COMMENT ON COLUMN "public"."shenyu_lock"."lock_key" IS 'lock_key';
+COMMENT ON COLUMN "public"."shenyu_lock"."region" IS 'region';
+COMMENT ON COLUMN "public"."shenyu_lock"."client_id" IS 'client_id';
+COMMENT ON COLUMN "public"."shenyu_lock"."created_date" IS 'created_date';
+
INSERT INTO "public"."resource" VALUES ('1347048240677269503', '1346777766301888512', 'SHENYU.PLUGIN.BATCH.OPENED', '', '', '', 2, 3, '', 1, 0, 'system:authen:open', 1, '2022-05-25 18:08:01', '2022-05-25 18:08:01');
INSERT INTO "public"."permission" VALUES ('1351007708748849151', '1346358560427216896', '1347048240677269503', '2022-05-25 18:08:01', '2022-05-25 18:08:01');
@@ -42,3 +46,25 @@ INSERT INTO "public"."resource" VALUES ('1386680049203195915', '1346777157943259
INSERT INTO "public"."resource" VALUES ('1386680049203195916', '1346777157943259136', 'SHENYU.COMMON.IMPORT', '', '', '', 2, 0, '', 1, 0, 'system:manager:importConfig', 1, '2022-05-25 18:08:01', '2022-05-25 18:08:01');
INSERT INTO "public"."permission" VALUES ('1386680049203195906', '1346358560427216896', '1386680049203195915', '2022-05-25 18:08:01', '2022-05-25 18:08:01');
INSERT INTO "public"."permission" VALUES ('1386680049203195907', '1346358560427216896', '1386680049203195916', '2022-05-25 18:08:01', '2022-05-25 18:08:01');
+
+
+-- ----------------------------
+-- Table structure for cluster_master
+-- ----------------------------
+DROP TABLE IF EXISTS "public"."cluster_master";
+CREATE TABLE "public"."cluster_master"
+(
+ "id" varchar(128) COLLATE "pg_catalog"."default" NOT NULL PRIMARY KEY,
+ "master_host" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
+ "master_port" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
+ "context_path" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
+ "date_created" timestamp(6) NOT NULL DEFAULT timezone('UTC-8'::text, (now())::timestamp(0) without time zone),
+ "date_updated" timestamp(6) NOT NULL DEFAULT timezone('UTC-8'::text, (now())::timestamp(0) without time zone)
+)
+;
+COMMENT ON COLUMN "public"."cluster_master"."id" IS 'primary key id';
+COMMENT ON COLUMN "public"."cluster_master"."master_host" IS 'master host';
+COMMENT ON COLUMN "public"."cluster_master"."master_port" IS 'master port';
+COMMENT ON COLUMN "public"."cluster_master"."context_path" IS 'master context_path';
+COMMENT ON COLUMN "public"."cluster_master"."date_created" IS 'create time';
+COMMENT ON COLUMN "public"."cluster_master"."date_updated" IS 'update time';
\ No newline at end of file
diff --git a/shenyu-admin/pom.xml b/shenyu-admin/pom.xml
index f699e2f57f99..b342868b9cb4 100644
--- a/shenyu-admin/pom.xml
+++ b/shenyu-admin/pom.xml
@@ -84,6 +84,11 @@
spring-integration-jdbc
+
+ org.springframework.integration
+ spring-integration-zookeeper
+
+
org.springframework.boot
spring-boot-starter-thymeleaf
@@ -298,7 +303,7 @@
${project.version}
-
+
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/ClusterConfiguration.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/ClusterConfiguration.java
new file mode 100644
index 000000000000..9768c5cb3c96
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/ClusterConfiguration.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.config;
+
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.config.properties.ClusterZookeeperProperties;
+import org.apache.shenyu.admin.mode.ShenyuRunningModeService;
+import org.apache.shenyu.admin.mode.cluster.filter.ClusterForwardFilter;
+import org.apache.shenyu.admin.mode.cluster.service.ClusterSelectMasterService;
+import org.apache.shenyu.admin.mode.cluster.service.ShenyuClusterService;
+import org.apache.shenyu.admin.service.impl.UpstreamCheckService;
+import org.apache.shenyu.admin.service.manager.LoadServiceDocEntry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * The type Cluster configuration.
+ */
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties({ClusterProperties.class, ClusterZookeeperProperties.class})
+@ConditionalOnProperty(value = {"shenyu.cluster.enabled"}, havingValue = "true", matchIfMissing = false)
+public class ClusterConfiguration {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ClusterConfiguration.class);
+
+ /**
+ * Shenyu running mode cluster service.
+ *
+ * @param shenyuClusterSelectMasterService shenyu cluster select master service
+ * @param upstreamCheckService upstream check service
+ * @param loadServiceDocEntry load service doc entry
+ * @param clusterProperties cluster properties
+ * @return Shenyu cluster service
+ */
+ @Bean(destroyMethod = "shutdown")
+ @ConditionalOnMissingBean
+ public ShenyuRunningModeService shenyuRunningModeService(final ClusterSelectMasterService shenyuClusterSelectMasterService,
+ final UpstreamCheckService upstreamCheckService,
+ final LoadServiceDocEntry loadServiceDocEntry,
+ final ClusterProperties clusterProperties) {
+ LOGGER.info("starting in cluster mode ...");
+ return new ShenyuClusterService(shenyuClusterSelectMasterService,
+ upstreamCheckService,
+ loadServiceDocEntry,
+ clusterProperties
+ );
+ }
+
+ /**
+ * Shenyu cluster forward filter.
+ *
+ * @return the Shenyu cluster forward filter
+ */
+ @Bean
+ public ClusterForwardFilter clusterForwardFilter() {
+ return new ClusterForwardFilter();
+ }
+
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/ClusterJdbcConfiguration.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/ClusterJdbcConfiguration.java
new file mode 100644
index 000000000000..bf71574187f6
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/ClusterJdbcConfiguration.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.config;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.config.properties.ClusterZookeeperProperties;
+import org.apache.shenyu.admin.mode.cluster.impl.jdbc.ClusterSelectMasterServiceJdbcImpl;
+import org.apache.shenyu.admin.mode.cluster.impl.jdbc.mapper.ClusterMasterMapper;
+import org.apache.shenyu.admin.mode.cluster.service.ClusterSelectMasterService;
+import org.apache.shenyu.common.utils.IpUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.jdbc.lock.DefaultLockRepository;
+import org.springframework.integration.jdbc.lock.JdbcLockRegistry;
+import org.springframework.integration.jdbc.lock.LockRepository;
+
+import javax.sql.DataSource;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The type Cluster jdbc configuration.
+ */
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties({ClusterProperties.class, ClusterZookeeperProperties.class})
+@ConditionalOnProperty(value = {"shenyu.cluster.type"}, havingValue = "jdbc", matchIfMissing = true)
+public class ClusterJdbcConfiguration {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ClusterJdbcConfiguration.class);
+
+ @Value("${server.servlet.context-path:}")
+ private String contextPath;
+
+ @Value("${server.port:}")
+ private String port;
+
+ /**
+ * Shenyu Admin distributed lock by spring-integration-jdbc.
+ *
+ * @param dataSource the dataSource
+ * @param clusterProperties the cluster properties
+ * @return defaultLockRepository
+ */
+ @Bean
+ public DefaultLockRepository defaultLockRepository(final DataSource dataSource,
+ final ClusterProperties clusterProperties) {
+ final String host = IpUtils.getHost();
+ String fullPath = host + ":" + port;
+ if (StringUtils.isNoneBlank(contextPath)) {
+ fullPath += contextPath;
+ }
+ DefaultLockRepository defaultLockRepository = new DefaultLockRepository(dataSource, fullPath);
+ defaultLockRepository.setPrefix("SHENYU_");
+ long millis = TimeUnit.SECONDS.toMillis(clusterProperties.getLockTtl());
+ defaultLockRepository.setTimeToLive(Long.valueOf(millis).intValue());
+ return defaultLockRepository;
+ }
+
+ /**
+ * Shenyu Admin distributed lock by spring-integration-jdbc.
+ *
+ * @param lockRepository the lockRepository
+ * @return the shenyu Admin register repository
+ */
+ @Bean
+ public JdbcLockRegistry jdbcLockRegistry(final LockRepository lockRepository) {
+ return new JdbcLockRegistry(lockRepository);
+ }
+
+ /**
+ * Shenyu select master service.
+ *
+ * @param clusterProperties the cluster properties
+ * @param jdbcLockRegistry the jdbc lock registry
+ * @param clusterMasterMapper the cluster master mapper
+ * @return the shenyu select master service
+ */
+ @Bean
+ public ClusterSelectMasterService clusterSelectMasterJdbcService(final ClusterProperties clusterProperties,
+ final JdbcLockRegistry jdbcLockRegistry,
+ final ClusterMasterMapper clusterMasterMapper) {
+ return new ClusterSelectMasterServiceJdbcImpl(clusterProperties, jdbcLockRegistry, clusterMasterMapper);
+ }
+
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/ClusterZookeeperConfiguration.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/ClusterZookeeperConfiguration.java
new file mode 100644
index 000000000000..5d4c7a5f0398
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/ClusterZookeeperConfiguration.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.config;
+
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.config.properties.ClusterZookeeperProperties;
+import org.apache.shenyu.admin.mode.cluster.impl.zookeeper.ClusterSelectMasterServiceZookeeperImpl;
+import org.apache.shenyu.admin.mode.cluster.impl.zookeeper.ClusterZookeeperClient;
+import org.apache.shenyu.admin.mode.cluster.impl.zookeeper.ClusterZookeeperConfig;
+import org.apache.shenyu.admin.mode.cluster.service.ClusterSelectMasterService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.zookeeper.lock.ZookeeperLockRegistry;
+
+/**
+ * The type Cluster zookeeper configuration.
+ */
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties({ClusterProperties.class, ClusterZookeeperProperties.class})
+@ConditionalOnProperty(value = {"shenyu.cluster.type"}, havingValue = "zookeeper", matchIfMissing = false)
+public class ClusterZookeeperConfiguration {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ClusterZookeeperConfiguration.class);
+
+ private static final String LOCK_PATH = "/shenyu-cluster-lock";
+
+ /**
+ * Shenyu Admin distributed lock by spring-integration-zookeeper.
+ *
+ * @param clusterZookeeperClient the cluster zookeeper client
+ * @return the shenyu Admin zookeeper lock registry
+ */
+ @Bean
+ public ZookeeperLockRegistry zookeeperLockRegistry(final ClusterZookeeperClient clusterZookeeperClient) {
+ return new ZookeeperLockRegistry(clusterZookeeperClient.getClient(), LOCK_PATH);
+ }
+
+ /**
+ * Shenyu cluster select master service.
+ *
+ * @param clusterProperties the cluster properties
+ * @param zookeeperLockRegistry the zookeeper lock registry
+ * @param clusterZookeeperClient cluster zookeeper client
+ * @return the shenyu cluster select master service
+ */
+ @Bean
+ public ClusterSelectMasterService clusterSelectMasterZookeeperService(final ClusterProperties clusterProperties,
+ final ZookeeperLockRegistry zookeeperLockRegistry,
+ final ClusterZookeeperClient clusterZookeeperClient) {
+ return new ClusterSelectMasterServiceZookeeperImpl(clusterProperties, zookeeperLockRegistry, clusterZookeeperClient);
+ }
+
+ /**
+ * register zkClient in spring ioc.
+ *
+ * @param clusterZookeeperProperties the zookeeper configuration
+ * @return ClusterZookeeperClient {@linkplain ClusterZookeeperClient}
+ */
+ @Bean
+ public ClusterZookeeperClient clusterZookeeperClient(final ClusterZookeeperProperties clusterZookeeperProperties) {
+ int sessionTimeout = clusterZookeeperProperties.getSessionTimeout();
+ int connectionTimeout = clusterZookeeperProperties.getConnectionTimeout();
+ ClusterZookeeperConfig zkConfig = new ClusterZookeeperConfig(clusterZookeeperProperties.getUrl());
+ zkConfig.setSessionTimeoutMilliseconds(sessionTimeout)
+ .setConnectionTimeoutMilliseconds(connectionTimeout);
+ ClusterZookeeperClient client = new ClusterZookeeperClient(zkConfig);
+ client.start();
+ return client;
+ }
+
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/RegisterCenterConfiguration.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/RegisterCenterConfiguration.java
index 79b06c15d877..8791da4712ae 100644
--- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/RegisterCenterConfiguration.java
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/RegisterCenterConfiguration.java
@@ -29,10 +29,6 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.integration.jdbc.lock.DefaultLockRepository;
-import org.springframework.integration.jdbc.lock.JdbcLockRegistry;
-import org.springframework.integration.jdbc.lock.LockRepository;
-import javax.sql.DataSource;
import org.springframework.transaction.PlatformTransactionManager;
import java.util.List;
@@ -89,27 +85,4 @@ public RegisterExecutionRepository registerExecutionRepository(final PlatformTra
return new PlatformTransactionRegisterExecutionRepository(platformTransactionManager, pluginMapper);
}
-
- /**
- * Shenyu Admin distributed lock by spring-integration-jdbc.
- *
- * @param dataSource the dataSource
- * @return defaultLockRepository
- */
- @Bean
- @ConfigurationProperties(prefix = "shenyu.distributed-lock")
- public DefaultLockRepository defaultLockRepository(final DataSource dataSource) {
- return new DefaultLockRepository(dataSource);
- }
-
- /**
- * Shenyu Admin distributed lock by spring-integration-jdbc.
- *
- * @param lockRepository the lockRepository
- * @return the shenyu Admin register repository
- */
- @Bean
- public JdbcLockRegistry jdbcLockRegistry(final LockRepository lockRepository) {
- return new JdbcLockRegistry(lockRepository);
- }
}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/StandaloneConfiguration.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/StandaloneConfiguration.java
new file mode 100644
index 000000000000..728cd50ffbf9
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/StandaloneConfiguration.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.config;
+
+import org.apache.shenyu.admin.mode.ShenyuRunningModeService;
+import org.apache.shenyu.admin.mode.standalone.ShenyuStandaloneService;
+import org.apache.shenyu.admin.service.impl.UpstreamCheckService;
+import org.apache.shenyu.admin.service.manager.LoadServiceDocEntry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * The type Standalone configuration.
+ */
+@Configuration(proxyBeanMethods = false)
+public class StandaloneConfiguration {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(StandaloneConfiguration.class);
+
+ /**
+ * Shenyu running mode standalone service.
+ *
+ * @param upstreamCheckService upstream check service
+ * @param loadServiceDocEntry load service doc entry
+ * @return Shenyu standalone service
+ */
+ @Bean(destroyMethod = "shutdown")
+ @ConditionalOnProperty(value = {"shenyu.cluster.enabled"}, havingValue = "false", matchIfMissing = true)
+ @ConditionalOnMissingBean
+ public ShenyuRunningModeService shenyuRunningModeService(final UpstreamCheckService upstreamCheckService,
+ final LoadServiceDocEntry loadServiceDocEntry) {
+ LOGGER.info("starting in standalone mode ...");
+ return new ShenyuStandaloneService(
+ upstreamCheckService,
+ loadServiceDocEntry
+ );
+ }
+
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/properties/ClusterProperties.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/properties/ClusterProperties.java
new file mode 100644
index 000000000000..589b72d181ca
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/properties/ClusterProperties.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.config.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.List;
+
+/**
+ * Cluster properties.
+ */
+@ConfigurationProperties(prefix = "shenyu.cluster")
+public class ClusterProperties {
+
+ /**
+ * Whether enabled cluster mode, default: false.
+ */
+ private boolean enabled;
+
+ /**
+ * the master select method type.
+ */
+ private String type;
+
+ /**
+ * the master schema (http/https).
+ */
+ private String schema = "http";
+
+ /**
+ * cluster forward uri list.
+ */
+ private List forwardList;
+
+ /**
+ * cluster select master task period.
+ */
+ private Long selectPeriod = 15L;
+
+ /**
+ * cluster master lock time to live.
+ */
+ private Long lockTtl = 15L;
+
+ /**
+ * Gets the value of enabled.
+ *
+ * @return the value of enabled
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Sets the enabled.
+ *
+ * @param enabled enabled
+ */
+ public void setEnabled(final boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ /**
+ * Get the select type.
+ *
+ * @return the select type
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * Set the select type.
+ *
+ * @param type the select type
+ */
+ public void setType(final String type) {
+ this.type = type;
+ }
+
+ /**
+ * Get the schema.
+ *
+ * @return schema
+ */
+ public String getSchema() {
+ return schema;
+ }
+
+ /**
+ * Set the schema.
+ *
+ * @param schema schema
+ */
+ public void setSchema(final String schema) {
+ this.schema = schema;
+ }
+
+ /**
+ * Gets the value of forwardList.
+ *
+ * @return the value of forwardList
+ */
+ public List getForwardList() {
+ return forwardList;
+ }
+
+ /**
+ * Sets the forwardList.
+ *
+ * @param forwardList forwardList
+ */
+ public void setForwardList(final List forwardList) {
+ this.forwardList = forwardList;
+ }
+
+ /**
+ * Gets the select master task period.
+ *
+ * @return select master task period
+ */
+ public Long getSelectPeriod() {
+ return selectPeriod;
+ }
+
+ /**
+ * Sets select master task period.
+ *
+ * @param selectPeriod select master task period
+ */
+ public void setSelectPeriod(final Long selectPeriod) {
+ this.selectPeriod = selectPeriod;
+ }
+
+ /**
+ * Gets the select master lock ttl.
+ *
+ * @return lock ttl
+ */
+ public Long getLockTtl() {
+ return lockTtl;
+ }
+
+ /**
+ * Sets select master lock ttl.
+ *
+ * @param lockTtl lock ttl
+ */
+ public void setLockTtl(final Long lockTtl) {
+ this.lockTtl = lockTtl;
+ }
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/properties/ClusterZookeeperProperties.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/properties/ClusterZookeeperProperties.java
new file mode 100644
index 000000000000..b96803ac1d43
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/config/properties/ClusterZookeeperProperties.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.config.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.StringJoiner;
+
+/**
+ * Cluster Zookeeper properties.
+ */
+@ConfigurationProperties(prefix = "shenyu.cluster.zookeeper")
+public class ClusterZookeeperProperties {
+
+ private String url;
+
+ private Integer sessionTimeout = 3000;
+
+ private Integer connectionTimeout = 3000;
+
+ private String serializer;
+
+ /**
+ * Get url.
+ *
+ * @return url
+ */
+ public String getUrl() {
+ return url;
+ }
+
+ /**
+ * Set url.
+ *
+ * @param url url
+ */
+ public void setUrl(final String url) {
+ this.url = url;
+ }
+
+ /**
+ * Get sessionTimeout.
+ *
+ * @return sessionTimeout
+ */
+ public Integer getSessionTimeout() {
+ return sessionTimeout;
+ }
+
+ /**
+ * Set sessionTimeout.
+ *
+ * @param sessionTimeout sessionTimeout
+ */
+ public void setSessionTimeout(final Integer sessionTimeout) {
+ this.sessionTimeout = sessionTimeout;
+ }
+
+ /**
+ * Get connectionTimeout.
+ *
+ * @return connectionTimeout
+ */
+ public Integer getConnectionTimeout() {
+ return connectionTimeout;
+ }
+
+ /**
+ * Set connectionTimeout.
+ *
+ * @param connectionTimeout connectionTimeout
+ */
+ public void setConnectionTimeout(final Integer connectionTimeout) {
+ this.connectionTimeout = connectionTimeout;
+ }
+
+ /**
+ * Get serializer.
+ *
+ * @return serializer
+ */
+ public String getSerializer() {
+ return serializer;
+ }
+
+ /**
+ * Set serializer.
+ *
+ * @param serializer serializer
+ */
+ public void setSerializer(final String serializer) {
+ this.serializer = serializer;
+ }
+
+ @Override
+ public String toString() {
+ return new StringJoiner(", ", ZookeeperProperties.class.getSimpleName() + "[", "]")
+ .add("url='" + url + "'")
+ .add("sessionTimeout=" + sessionTimeout)
+ .add("connectionTimeout=" + connectionTimeout)
+ .add("serializer='" + serializer + "'")
+ .toString();
+ }
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/ApplicationStartListener.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/ApplicationStartListener.java
index 96ca388b0c3e..67d5cd0a9d99 100644
--- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/ApplicationStartListener.java
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/ApplicationStartListener.java
@@ -17,9 +17,8 @@
package org.apache.shenyu.admin.listener;
-import javax.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
-import org.apache.shenyu.admin.service.manager.LoadServiceDocEntry;
+import org.apache.shenyu.admin.mode.ShenyuRunningModeService;
import org.apache.shenyu.admin.utils.ShenyuDomain;
import org.apache.shenyu.common.utils.IpUtils;
import org.springframework.beans.factory.annotation.Value;
@@ -27,19 +26,21 @@
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
+import javax.annotation.Resource;
+
/**
* ApplicationStartListener.
*/
@Component
public class ApplicationStartListener implements ApplicationListener {
-
- @Resource
- private LoadServiceDocEntry loadServiceDocEntry;
-
+
@Value("${server.servlet.context-path:}")
private String contextPath;
-
+
+ @Resource
+ private ShenyuRunningModeService shenyuRunningModeService;
+
@Override
public void onApplicationEvent(final WebServerInitializedEvent event) {
int port = event.getWebServer().getPort();
@@ -50,6 +51,7 @@ public void onApplicationEvent(final WebServerInitializedEvent event) {
} else {
ShenyuDomain.getInstance().setHttpPath(domain);
}
- loadServiceDocEntry.loadApiDocument();
+
+ shenyuRunningModeService.start(host, port, contextPath);
}
}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/DataChangedEventDispatcher.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/DataChangedEventDispatcher.java
index 262035a89ea1..0089400258db 100644
--- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/DataChangedEventDispatcher.java
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/DataChangedEventDispatcher.java
@@ -17,42 +17,70 @@
package org.apache.shenyu.admin.listener;
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.mode.cluster.service.ClusterSelectMasterService;
import org.apache.shenyu.admin.service.manager.LoadServiceDocEntry;
import org.apache.shenyu.common.dto.AppAuthData;
+import org.apache.shenyu.common.dto.DiscoverySyncData;
+import org.apache.shenyu.common.dto.MetaData;
import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.dto.ProxySelectorData;
import org.apache.shenyu.common.dto.RuleData;
import org.apache.shenyu.common.dto.SelectorData;
-import org.apache.shenyu.common.dto.MetaData;
-import org.apache.shenyu.common.dto.DiscoverySyncData;
-import org.apache.shenyu.common.dto.ProxySelectorData;
+import org.apache.shenyu.common.utils.JsonUtils;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
+import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
+import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
/**
* Event forwarders, which forward the changed events to each ConfigEventListener.
*/
@Component
public class DataChangedEventDispatcher implements ApplicationListener, InitializingBean {
-
+
+ private static final Logger LOG = LoggerFactory.getLogger(DataChangedEventDispatcher.class);
+
private final ApplicationContext applicationContext;
-
+
private List listeners;
-
+
+ @Resource
+ private ClusterProperties clusterProperties;
+
+ @Resource
+ @Nullable
+ private ClusterSelectMasterService shenyuClusterSelectMasterService;
+
public DataChangedEventDispatcher(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
-
+
@Override
@SuppressWarnings("unchecked")
- public void onApplicationEvent(final DataChangedEvent event) {
+ public void onApplicationEvent(@NotNull final DataChangedEvent event) {
for (DataChangedListener listener : listeners) {
+ if ((!(listener instanceof AbstractDataChangedListener))
+ && clusterProperties.isEnabled()
+ && Objects.nonNull(shenyuClusterSelectMasterService)
+ && !shenyuClusterSelectMasterService.isMaster()) {
+ LOG.info("received DataChangedEvent, not master, pass");
+ return;
+ }
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("received DataChangedEvent, dispatching, event:{}", JsonUtils.toJson(event));
+ }
switch (event.getGroupKey()) {
case APP_AUTH:
listener.onAppAuthChanged((List) event.getSource(), event.getEventType());
@@ -81,7 +109,7 @@ public void onApplicationEvent(final DataChangedEvent event) {
}
}
}
-
+
@Override
public void afterPropertiesSet() {
Collection listenerBeans = applicationContext.getBeansOfType(DataChangedListener.class).values();
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/websocket/WebsocketCollector.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/websocket/WebsocketCollector.java
index 81cfdae0f1f6..9514ee985e37 100644
--- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/websocket/WebsocketCollector.java
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/websocket/WebsocketCollector.java
@@ -17,12 +17,18 @@
package org.apache.shenyu.admin.listener.websocket;
+import com.google.common.collect.Maps;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.mode.cluster.service.ClusterSelectMasterService;
import org.apache.shenyu.admin.service.SyncDataService;
import org.apache.shenyu.admin.spring.SpringBeanUtils;
import org.apache.shenyu.admin.utils.ThreadLocalUtils;
+import org.apache.shenyu.common.constant.RunningModeConstants;
import org.apache.shenyu.common.enums.DataEventTypeEnum;
+import org.apache.shenyu.common.enums.RunningModeEnum;
+import org.apache.shenyu.common.utils.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -66,6 +72,9 @@ public void onOpen(final Session session) {
}
private static String getClientIp(final Session session) {
+ if (!session.isOpen()) {
+ return StringUtils.EMPTY;
+ }
Map userProperties = session.getUserProperties();
if (MapUtils.isEmpty(userProperties)) {
return StringUtils.EMPTY;
@@ -84,17 +93,49 @@ private static String getClientIp(final Session session) {
*/
@OnMessage
public void onMessage(final String message, final Session session) {
- if (!Objects.equals(message, DataEventTypeEnum.MYSELF.name())) {
+ if (!Objects.equals(message, DataEventTypeEnum.MYSELF.name())
+ && !Objects.equals(message, DataEventTypeEnum.RUNNING_MODE.name())) {
return;
}
- try {
- ThreadLocalUtils.put(SESSION_KEY, session);
- SpringBeanUtils.getInstance().getBean(SyncDataService.class).syncAll(DataEventTypeEnum.MYSELF);
- } finally {
- ThreadLocalUtils.clear();
- }
+ if (Objects.equals(message, DataEventTypeEnum.RUNNING_MODE.name())) {
+ LOG.info("websocket fetching running mode info...");
+ // check if this node is master
+ boolean isMaster = true;
+ String runningMode = RunningModeEnum.STANDALONE.name();
+ String masterUrl = StringUtils.EMPTY;
+ ClusterProperties clusterProperties = SpringBeanUtils.getInstance().getBean(ClusterProperties.class);
+ if (clusterProperties.isEnabled()) {
+ ClusterSelectMasterService clusterSelectMasterService = SpringBeanUtils.getInstance().getBean(ClusterSelectMasterService.class);
+ runningMode = RunningModeEnum.CLUSTER.name();
+ isMaster = clusterSelectMasterService.isMaster();
+ masterUrl = clusterSelectMasterService.getMasterUrl();
+ }
+ Map map = Maps.newHashMap();
+ map.put(RunningModeConstants.EVENT_TYPE, DataEventTypeEnum.RUNNING_MODE.name());
+ map.put(RunningModeConstants.IS_MASTER, isMaster);
+ map.put(RunningModeConstants.RUNNING_MODE, runningMode);
+ map.put(RunningModeConstants.MASTER_URL, masterUrl
+ .replace("http", "ws")
+ .replace("https", "ws")
+ .concat("/websocket"));
+ if (isMaster) {
+ ThreadLocalUtils.put(SESSION_KEY, session);
+ }
+ sendMessageBySession(session, JsonUtils.toJson(map));
+ return;
+ }
+
+ if (Objects.equals(message, DataEventTypeEnum.MYSELF.name())) {
+ try {
+ ThreadLocalUtils.put(SESSION_KEY, session);
+ SpringBeanUtils.getInstance().getBean(SyncDataService.class).syncAll(DataEventTypeEnum.MYSELF);
+ } finally {
+ ThreadLocalUtils.clear();
+ }
+ }
+
}
/**
@@ -112,7 +153,7 @@ public void onClose(final Session session) {
* On error.
*
* @param session the session
- * @param error the error
+ * @param error the error
*/
@OnError
public void onError(final Session session, final Throwable error) {
@@ -124,7 +165,7 @@ public void onError(final Session session, final Throwable error) {
* Send.
*
* @param message the message
- * @param type the type
+ * @param type the type
*/
public static void send(final String message, final DataEventTypeEnum type) {
if (StringUtils.isBlank(message)) {
@@ -134,7 +175,11 @@ public static void send(final String message, final DataEventTypeEnum type) {
if (DataEventTypeEnum.MYSELF == type) {
Session session = (Session) ThreadLocalUtils.get(SESSION_KEY);
if (Objects.nonNull(session)) {
- sendMessageBySession(session, message);
+ if (session.isOpen()) {
+ sendMessageBySession(session, message);
+ } else {
+ SESSION_SET.remove(session);
+ }
}
} else {
SESSION_SET.forEach(session -> sendMessageBySession(session, message));
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/ShenyuRunningModeService.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/ShenyuRunningModeService.java
new file mode 100644
index 000000000000..9da90c1ca544
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/ShenyuRunningModeService.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.mode;
+
+/**
+ * The Shenyu Running Mode service.
+ */
+public interface ShenyuRunningModeService {
+
+ /**
+ * server satrt method.
+ *
+ * @param host server host
+ * @param port server port
+ * @param contextPath server contextPath
+ */
+ void start(String host, int port, String contextPath);
+
+ /**
+ * server shutdown method.
+ */
+ void shutdown();
+
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/filter/ClusterForwardFilter.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/filter/ClusterForwardFilter.java
new file mode 100644
index 000000000000..834e3e1a96c5
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/filter/ClusterForwardFilter.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.mode.cluster.filter;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.mode.cluster.service.ClusterSelectMasterService;
+import org.apache.shenyu.admin.model.dto.ClusterMasterDTO;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.server.ServletServerHttpRequest;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.PathMatcher;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.annotation.Resource;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * Cluster forward filter.
+ */
+public class ClusterForwardFilter extends OncePerRequestFilter {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ClusterForwardFilter.class);
+
+ private static final PathMatcher PATH_MATCHER = new AntPathMatcher();
+
+ @Resource
+ private RestTemplate restTemplate;
+
+ @Resource
+ private ClusterSelectMasterService clusterSelectMasterService;
+
+ @Resource
+ private ClusterProperties clusterProperties;
+
+ @Override
+ protected void doFilterInternal(@NotNull final HttpServletRequest request,
+ @NotNull final HttpServletResponse response,
+ @NotNull final FilterChain filterChain) throws ServletException, IOException {
+ String method = request.getMethod();
+ if (StringUtils.equals(HttpMethod.OPTIONS.name(), method)) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ if (clusterSelectMasterService.isMaster()) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+ // this node is not master
+ String uri = request.getRequestURI();
+ String requestContextPath = request.getContextPath();
+ String replaced = uri.replaceAll(requestContextPath, "");
+ boolean anyMatch = clusterProperties.getForwardList()
+ .stream().anyMatch(x -> PATH_MATCHER.match(x, replaced));
+ if (!anyMatch) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+ // cluster forward request to master
+ forwardRequest(request, response);
+ }
+
+ private void forwardRequest(final HttpServletRequest request,
+ final HttpServletResponse response) throws IOException {
+ String targetUrl = getForwardingUrl(request);
+
+ LOG.info("forwarding current uri: {} request to target url: {}", request.getRequestURI(), targetUrl);
+ // Create request entity
+ HttpHeaders headers = new HttpHeaders();
+ // Copy request headers
+ copyHeaders(request, headers);
+ HttpEntity requestEntity = new HttpEntity<>(getBody(request), headers);
+ // Send request
+ ResponseEntity responseEntity = restTemplate.exchange(targetUrl, HttpMethod.valueOf(request.getMethod()), requestEntity, byte[].class);
+
+ // Set response status and headers
+ response.setStatus(responseEntity.getStatusCodeValue());
+ // Copy response headers
+ copyHeaders(responseEntity.getHeaders(), response);
+ // fix cors error
+ response.addHeader("Access-Control-Allow-Origin", "*");
+ response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
+ // write response back
+ IOUtils.copy(new ByteArrayInputStream(Objects.requireNonNull(responseEntity.getBody())), response.getOutputStream());
+ response.getOutputStream().flush();
+ }
+
+ @NotNull
+ private String getForwardingUrl(final HttpServletRequest request) {
+ ClusterMasterDTO master = clusterSelectMasterService.getMaster();
+ String host = master.getMasterHost();
+ String port = master.getMasterPort();
+ String masterContextPath = master.getContextPath();
+
+ UriComponentsBuilder builder = UriComponentsBuilder
+ .fromHttpRequest(new ServletServerHttpRequest(request))
+ .host(host)
+ .port(port);
+ String originalPath = builder.build().getPath();
+
+ if (StringUtils.isNotEmpty(originalPath)) {
+ // remove current context path
+ String currentContextPath = request.getContextPath();
+ if (StringUtils.isNotEmpty(currentContextPath) && originalPath.startsWith(currentContextPath)) {
+ originalPath = originalPath.substring(originalPath.indexOf(currentContextPath) + currentContextPath.length());
+ }
+ }
+ if (StringUtils.isNoneBlank(masterContextPath)) {
+ originalPath = masterContextPath + originalPath;
+ }
+ builder.replacePath(originalPath);
+
+ return builder.toUriString();
+ }
+
+ private void copyHeaders(final HttpServletRequest request, final HttpHeaders headers) {
+ Collections.list(request.getHeaderNames())
+ .forEach(headerName -> headers.add(headerName, removeSpecial(request.getHeader(headerName))));
+ }
+
+ private void copyHeaders(final HttpHeaders sourceHeaders, final HttpServletResponse response) {
+ sourceHeaders.forEach((headerName, headerValues) -> {
+ String name = removeSpecial(headerName);
+ if (!response.containsHeader(name)) {
+ headerValues.forEach(headerValue -> {
+ response.addHeader(name, removeSpecial(headerValue));
+ });
+ }
+ });
+ }
+
+ private String removeSpecial(final String str) {
+ return str.replaceAll("\r", "").replaceAll("\n", "");
+ }
+
+ private byte[] getBody(final HttpServletRequest request) throws IOException {
+ InputStream is = request.getInputStream();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int bytesRead;
+ while ((bytesRead = is.read(buffer)) != -1) {
+ baos.write(buffer, 0, bytesRead);
+ }
+ return baos.toByteArray();
+ }
+
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/jdbc/ClusterSelectMasterServiceJdbcImpl.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/jdbc/ClusterSelectMasterServiceJdbcImpl.java
new file mode 100644
index 000000000000..02854d6cf04e
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/jdbc/ClusterSelectMasterServiceJdbcImpl.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.mode.cluster.impl.jdbc;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.mode.cluster.impl.jdbc.mapper.ClusterMasterMapper;
+import org.apache.shenyu.admin.mode.cluster.service.ClusterSelectMasterService;
+import org.apache.shenyu.admin.model.dto.ClusterMasterDTO;
+import org.apache.shenyu.admin.model.entity.ClusterMasterDO;
+import org.apache.shenyu.admin.transfer.ClusterMasterTransfer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.integration.jdbc.lock.JdbcLockRegistry;
+
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.Objects;
+import java.util.concurrent.locks.Lock;
+
+public class ClusterSelectMasterServiceJdbcImpl implements ClusterSelectMasterService {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ClusterSelectMasterServiceJdbcImpl.class);
+
+ private static final String MASTER_LOCK_KEY = "shenyu_cluster_lock:master";
+
+ private static final String MASTER_ID = "1";
+
+ private final ClusterProperties clusterProperties;
+
+ private final JdbcLockRegistry jdbcLockRegistry;
+
+ private final ClusterMasterMapper clusterMasterMapper;
+
+ private final Lock clusterMasterLock;
+
+ private volatile boolean masterFlag;
+
+ public ClusterSelectMasterServiceJdbcImpl(final ClusterProperties clusterProperties,
+ final JdbcLockRegistry jdbcLockRegistry,
+ final ClusterMasterMapper clusterMasterMapper) {
+ this.clusterProperties = clusterProperties;
+ this.jdbcLockRegistry = jdbcLockRegistry;
+ this.clusterMasterMapper = clusterMasterMapper;
+ this.clusterMasterLock = jdbcLockRegistry.obtain(MASTER_LOCK_KEY);
+ }
+
+ @Override
+ public boolean selectMaster() {
+ masterFlag = clusterMasterLock.tryLock();
+ LOG.info("select master result: {}", masterFlag);
+ return masterFlag;
+ }
+
+ @Override
+ public boolean selectMaster(final String masterHost, final String masterPort, final String contextPath) {
+ masterFlag = clusterMasterLock.tryLock();
+ if (masterFlag) {
+ Timestamp now = Timestamp.valueOf(LocalDateTime.now());
+ ClusterMasterDO masterDO = ClusterMasterDO.builder()
+ .id(MASTER_ID)
+ .masterHost(masterHost)
+ .masterPort(masterPort)
+ .contextPath(contextPath)
+ .dateCreated(now)
+ .dateUpdated(now)
+ .build();
+ try {
+ clusterMasterMapper.insert(masterDO);
+ } catch (Exception e) {
+ clusterMasterMapper.updateSelective(masterDO);
+ }
+ }
+ return masterFlag;
+ }
+
+ @Override
+ public boolean checkMasterStatus() throws IllegalStateException {
+ if (masterFlag) {
+ jdbcLockRegistry.renewLock(MASTER_LOCK_KEY);
+ }
+ return masterFlag;
+ }
+
+ @Override
+ public boolean releaseMaster() {
+ if (masterFlag) {
+ clusterMasterLock.unlock();
+ masterFlag = false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isMaster() {
+ if (!clusterProperties.isEnabled()) {
+ return true;
+ }
+ return masterFlag;
+ }
+
+ @Override
+ public ClusterMasterDTO getMaster() {
+ ClusterMasterDO masterDO = clusterMasterMapper.selectById(MASTER_ID);
+ return Objects.isNull(masterDO) ? new ClusterMasterDTO() : ClusterMasterTransfer.INSTANCE.mapToDTO(masterDO);
+ }
+
+ @Override
+ public String getMasterUrl() {
+ ClusterMasterDO master = clusterMasterMapper.selectById(MASTER_ID);
+ String contextPath = master.getContextPath();
+ if (StringUtils.isEmpty(contextPath)) {
+ return clusterProperties.getSchema() + "://" + master.getMasterHost() + ":" + master.getMasterPort();
+ }
+ if (contextPath.startsWith("/")) {
+ return clusterProperties.getSchema() + "://" + master.getMasterHost() + ":" + master.getMasterPort() + contextPath;
+ }
+ return clusterProperties.getSchema() + "://" + master.getMasterHost() + ":" + master.getMasterPort() + "/" + contextPath;
+ }
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/jdbc/mapper/ClusterMasterMapper.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/jdbc/mapper/ClusterMasterMapper.java
new file mode 100644
index 000000000000..646bc94698ac
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/jdbc/mapper/ClusterMasterMapper.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.mode.cluster.impl.jdbc.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.shenyu.admin.model.entity.ClusterMasterDO;
+
+/**
+ * The interface Cluster Master mapper.
+ */
+@Mapper
+public interface ClusterMasterMapper {
+
+ /**
+ * insert cluster master.
+ *
+ * @param clusterMasterDO {@linkplain ClusterMasterDO}
+ * @return rows int
+ */
+ int insert(ClusterMasterDO clusterMasterDO);
+
+ /**
+ * update cluster master.
+ *
+ * @param clusterMasterDO {@linkplain ClusterMasterDO}
+ * @return rows int
+ */
+ int updateSelective(ClusterMasterDO clusterMasterDO);
+
+ /**
+ * Count with condition.
+ *
+ * @param clusterMasterDO condition
+ * @return The value of count result
+ */
+ long count(ClusterMasterDO clusterMasterDO);
+
+ /**
+ * select by id.
+ *
+ * @param id primary key.
+ * @return ClusterMasterDO
+ */
+ ClusterMasterDO selectById(String id);
+
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/zookeeper/ClusterSelectMasterServiceZookeeperImpl.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/zookeeper/ClusterSelectMasterServiceZookeeperImpl.java
new file mode 100644
index 000000000000..14f158c224b3
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/zookeeper/ClusterSelectMasterServiceZookeeperImpl.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.mode.cluster.impl.zookeeper;
+
+import com.google.common.collect.Maps;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.mode.cluster.service.ClusterSelectMasterService;
+import org.apache.shenyu.admin.model.dto.ClusterMasterDTO;
+import org.apache.shenyu.common.utils.JsonUtils;
+import org.apache.zookeeper.CreateMode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.integration.zookeeper.lock.ZookeeperLockRegistry;
+
+import java.util.Map;
+import java.util.concurrent.locks.Lock;
+
+public class ClusterSelectMasterServiceZookeeperImpl implements ClusterSelectMasterService {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ClusterSelectMasterServiceZookeeperImpl.class);
+
+ private static final String MASTER_LOCK_KEY = "shenyu_cluster_lock/master";
+
+ private static final String MASTER_INFO = "/shenyu_cluster_lock/master/info";
+
+ private final ClusterProperties clusterProperties;
+
+ private final ClusterZookeeperClient clusterZookeeperClient;
+
+ private final Lock clusterMasterLock;
+
+ private volatile boolean masterFlag;
+
+ public ClusterSelectMasterServiceZookeeperImpl(final ClusterProperties clusterProperties,
+ final ZookeeperLockRegistry zookeeperLockRegistry,
+ final ClusterZookeeperClient clusterZookeeperClient) {
+ this.clusterProperties = clusterProperties;
+ this.clusterZookeeperClient = clusterZookeeperClient;
+ this.clusterMasterLock = zookeeperLockRegistry.obtain(MASTER_LOCK_KEY);
+ }
+
+ @Override
+ public boolean selectMaster() {
+ masterFlag = clusterMasterLock.tryLock();
+ return masterFlag;
+ }
+
+ @Override
+ public boolean selectMaster(final String masterHost, final String masterPort, final String contextPath) {
+ masterFlag = clusterMasterLock.tryLock();
+ if (masterFlag) {
+ Map masterInfo = Maps.newHashMap();
+ masterInfo.put("masterHost", masterHost);
+ masterInfo.put("masterPort", masterPort);
+ masterInfo.put("contextPath", contextPath);
+ clusterZookeeperClient.createOrUpdate(MASTER_INFO, JsonUtils.toJson(masterInfo), CreateMode.PERSISTENT);
+ }
+ return masterFlag;
+ }
+
+ @Override
+ public boolean checkMasterStatus() {
+ return masterFlag;
+ }
+
+ @Override
+ public boolean releaseMaster() {
+ if (masterFlag) {
+ clusterMasterLock.unlock();
+ masterFlag = false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isMaster() {
+ if (!clusterProperties.isEnabled()) {
+ return true;
+ }
+ return masterFlag;
+ }
+
+ @Override
+ public ClusterMasterDTO getMaster() {
+ String masterInfoJson = clusterZookeeperClient.getDirectly(MASTER_INFO);
+ return JsonUtils.jsonToObject(masterInfoJson, ClusterMasterDTO.class);
+ }
+
+ @Override
+ public String getMasterUrl() {
+ String masterInfoJson = clusterZookeeperClient.getDirectly(MASTER_INFO);
+ ClusterMasterDTO master = JsonUtils.jsonToObject(masterInfoJson, ClusterMasterDTO.class);
+ if (StringUtils.isEmpty(master.getContextPath())) {
+ return clusterProperties.getSchema() + "://" + master.getMasterHost() + ":" + master.getMasterPort();
+ }
+ return clusterProperties.getSchema() + "://" + master.getMasterHost() + ":" + master.getMasterPort() + "/" + master.getContextPath();
+ }
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/zookeeper/ClusterZookeeperClient.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/zookeeper/ClusterZookeeperClient.java
new file mode 100644
index 000000000000..cf047b491794
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/zookeeper/ClusterZookeeperClient.java
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.mode.cluster.impl.zookeeper;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.cache.ChildData;
+import org.apache.curator.framework.recipes.cache.TreeCache;
+import org.apache.curator.framework.recipes.cache.TreeCacheListener;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+import org.apache.curator.utils.CloseableUtils;
+import org.apache.shenyu.common.exception.ShenyuException;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.zookeeper.CreateMode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class ClusterZookeeperClient implements AutoCloseable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ClusterZookeeperClient.class);
+
+ private final ClusterZookeeperConfig config;
+
+ private final CuratorFramework client;
+
+ private final Map caches = new ConcurrentHashMap<>();
+
+ public ClusterZookeeperClient(final ClusterZookeeperConfig zookeeperConfig) {
+ this.config = zookeeperConfig;
+ ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(config.getBaseSleepTimeMilliseconds(), config.getMaxRetries(), config.getMaxSleepTimeMilliseconds());
+
+ CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder()
+ .connectString(config.getServerLists())
+ .retryPolicy(retryPolicy)
+ .connectionTimeoutMs(config.getConnectionTimeoutMilliseconds())
+ .sessionTimeoutMs(config.getSessionTimeoutMilliseconds())
+ .namespace(config.getNamespace());
+
+ if (!StringUtils.isEmpty(config.getDigest())) {
+ builder.authorization("digest", config.getDigest().getBytes(StandardCharsets.UTF_8));
+ }
+
+ this.client = builder.build();
+ }
+
+ /**
+ * start.
+ */
+ public void start() {
+ this.client.start();
+ try {
+ this.client.blockUntilConnected();
+ } catch (InterruptedException e) {
+ LOGGER.warn("Interrupted during zookeeper client starting.");
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * start.
+ */
+ @Override
+ public void close() {
+ // close all caches
+ for (Map.Entry cache : caches.entrySet()) {
+ CloseableUtils.closeQuietly(cache.getValue());
+ }
+ // close client
+ CloseableUtils.closeQuietly(client);
+ }
+
+ /**
+ * get curator framework.
+ *
+ * @return curator framework client.
+ */
+ public CuratorFramework getClient() {
+ return client;
+ }
+
+ /**
+ * check if key exist.
+ *
+ * @param key zookeeper path
+ * @return if exist.
+ */
+ public boolean isExist(final String key) {
+ try {
+ return null != client.checkExists().forPath(key);
+ } catch (Exception e) {
+ throw new ShenyuException(e);
+ }
+ }
+
+ /**
+ * get from zk directly.
+ *
+ * @param key zookeeper path
+ * @return value.
+ */
+ public String getDirectly(final String key) {
+ try {
+ byte[] ret = client.getData().forPath(key);
+ return Objects.isNull(ret) ? null : new String(ret, StandardCharsets.UTF_8);
+ } catch (Exception e) {
+ throw new ShenyuException(e);
+ }
+ }
+
+ /**
+ * get value for specific key.
+ *
+ * @param key zookeeper path
+ * @return value.
+ */
+ public String get(final String key) {
+ TreeCache cache = findFromcache(key);
+ if (Objects.isNull(cache)) {
+ return getDirectly(key);
+ }
+ ChildData data = cache.getCurrentData(key);
+ if (Objects.isNull(data)) {
+ return getDirectly(key);
+ }
+ return Objects.isNull(data.getData()) ? null : new String(data.getData(), StandardCharsets.UTF_8);
+ }
+
+ /**
+ * create or update key with value.
+ *
+ * @param key zookeeper path key.
+ * @param value string value.
+ * @param mode creation mode.
+ */
+ public void createOrUpdate(final String key, final String value, final CreateMode mode) {
+ String val = StringUtils.isEmpty(value) ? "" : value;
+ try {
+ client.create().orSetData().creatingParentsIfNeeded().withMode(mode).forPath(key, val.getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ throw new ShenyuException(e);
+ }
+ }
+
+ /**
+ * create or update key with value.
+ *
+ * @param key zookeeper path key.
+ * @param value object value.
+ * @param mode creation mode.
+ */
+ public void createOrUpdate(final String key, final Object value, final CreateMode mode) {
+ if (value != null) {
+ String val = GsonUtils.getInstance().toJson(value);
+ createOrUpdate(key, val, mode);
+ } else {
+ createOrUpdate(key, "", mode);
+ }
+ }
+
+ /**
+ * delete a node with specific key.
+ *
+ * @param key zookeeper path key.
+ */
+ public void delete(final String key) {
+ try {
+ client.delete().guaranteed().deletingChildrenIfNeeded().forPath(key);
+ } catch (Exception e) {
+ throw new ShenyuException(e);
+ }
+ }
+
+ /**
+ * get children with specific key.
+ *
+ * @param key zookeeper key.
+ * @return children node name.
+ */
+ public List getChildren(final String key) {
+ try {
+ return client.getChildren().forPath(key);
+ } catch (Exception e) {
+ LOGGER.error("zookeeper get child error=", e);
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * get created cache.
+ * @param path path.
+ * @return cache.
+ */
+ public TreeCache getCache(final String path) {
+ return caches.get(path);
+ }
+
+ /**
+ * add new curator cache.
+ * @param path path.
+ * @param listeners listeners.
+ * @return cache.
+ */
+ public TreeCache addCache(final String path, final TreeCacheListener... listeners) {
+ TreeCache cache = TreeCache.newBuilder(client, path).build();
+ caches.put(path, cache);
+ if (ArrayUtils.isNotEmpty(listeners)) {
+ for (TreeCacheListener listener : listeners) {
+ cache.getListenable().addListener(listener);
+ }
+ }
+ try {
+ cache.start();
+ } catch (Exception e) {
+ throw new ShenyuException("failed to add curator cache.", e);
+ }
+ return cache;
+ }
+
+ /**
+ * find cache with key.
+ * @param key key.
+ * @return cache.
+ */
+ private TreeCache findFromcache(final String key) {
+ for (Map.Entry cache : caches.entrySet()) {
+ if (key.startsWith(cache.getKey())) {
+ return cache.getValue();
+ }
+ }
+ return null;
+ }
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/zookeeper/ClusterZookeeperConfig.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/zookeeper/ClusterZookeeperConfig.java
new file mode 100644
index 000000000000..9f256e6c6956
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/impl/zookeeper/ClusterZookeeperConfig.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.mode.cluster.impl.zookeeper;
+
+public class ClusterZookeeperConfig {
+
+ /**
+ * zookeeper server list.
+ * e.g. host1:2181,host2:2181
+ */
+ private final String serverLists;
+
+ /**
+ * zookeeper namespace.
+ */
+ private String namespace = "";
+
+ /**
+ * initial amount of time to wait between retries.
+ */
+ private int baseSleepTimeMilliseconds = 1000;
+
+ /**
+ * max time in ms to sleep on each retry.
+ */
+ private int maxSleepTimeMilliseconds = Integer.MAX_VALUE;
+
+ /**
+ * max number of times to retry.
+ */
+ private int maxRetries = 3;
+
+ /**
+ * session timeout.
+ */
+ private int sessionTimeoutMilliseconds = 60 * 1000;
+
+ /**
+ * connection timeout.
+ */
+ private int connectionTimeoutMilliseconds = 15 * 1000;
+
+ /**
+ * auth token digest. no auth by default.
+ */
+ private String digest;
+
+ public ClusterZookeeperConfig(final String serverLists) {
+ this.serverLists = serverLists;
+ }
+
+ /**
+ * get zookeeper server list.
+ * @return server list.
+ */
+ public String getServerLists() {
+ return serverLists;
+ }
+
+ /**
+ * set namespace.
+ * @param namespace zk namespace
+ * @return zk config
+ */
+ public ClusterZookeeperConfig setNamespace(final String namespace) {
+ this.namespace = namespace;
+ return this;
+ }
+
+ /**
+ * get namespace.
+ * @return namespace
+ */
+ public String getNamespace() {
+ return namespace;
+ }
+
+ /**
+ * get base sleep time.
+ * @return base sleep time.
+ */
+ public int getBaseSleepTimeMilliseconds() {
+ return baseSleepTimeMilliseconds;
+ }
+
+ /**
+ * set base sleep time.
+ * @param baseSleepTimeMilliseconds base sleep time in milliseconds.
+ * @return zk config.
+ */
+ public ClusterZookeeperConfig setBaseSleepTimeMilliseconds(final int baseSleepTimeMilliseconds) {
+ this.baseSleepTimeMilliseconds = baseSleepTimeMilliseconds;
+ return this;
+ }
+
+ /**
+ * get max sleep time.
+ * @return max sleep time
+ */
+ public int getMaxSleepTimeMilliseconds() {
+ return maxSleepTimeMilliseconds;
+ }
+
+ /**
+ * set max sleep time.
+ * @param maxSleepTimeMilliseconds max sleep time.
+ * @return zk config.
+ */
+ public ClusterZookeeperConfig setMaxSleepTimeMilliseconds(final int maxSleepTimeMilliseconds) {
+ this.maxSleepTimeMilliseconds = maxSleepTimeMilliseconds;
+ return this;
+ }
+
+ /**
+ * get max retries.
+ * @return max retries
+ */
+ public int getMaxRetries() {
+ return maxRetries;
+ }
+
+ /**
+ * set max retries count.
+ * @param maxRetries max retries
+ * @return zk config.
+ */
+ public ClusterZookeeperConfig setMaxRetries(final int maxRetries) {
+ this.maxRetries = maxRetries;
+ return this;
+ }
+
+ /**
+ * get session timeout in milliseconds.
+ * @return session timeout.
+ */
+ public int getSessionTimeoutMilliseconds() {
+ return sessionTimeoutMilliseconds;
+ }
+
+ /**
+ * set session timeout in milliseconds.
+ * @param sessionTimeoutMilliseconds session timeout
+ * @return zk config.
+ */
+ public ClusterZookeeperConfig setSessionTimeoutMilliseconds(final int sessionTimeoutMilliseconds) {
+ this.sessionTimeoutMilliseconds = sessionTimeoutMilliseconds;
+ return this;
+ }
+
+ /**
+ * get connection timeout in milliseconds.
+ * @return connection timeout.
+ */
+ public int getConnectionTimeoutMilliseconds() {
+ return connectionTimeoutMilliseconds;
+ }
+
+ /**
+ * set connection timeout in milliseconds.
+ * @param connectionTimeoutMilliseconds connection timeout.
+ * @return zk config.
+ */
+ public ClusterZookeeperConfig setConnectionTimeoutMilliseconds(final int connectionTimeoutMilliseconds) {
+ this.connectionTimeoutMilliseconds = connectionTimeoutMilliseconds;
+ return this;
+ }
+
+ /**
+ * get digest.
+ * @return digest.
+ */
+ public String getDigest() {
+ return digest;
+ }
+
+ /**
+ * set digest.
+ * @param digest digest
+ * @return zk config.
+ */
+ public ClusterZookeeperConfig setDigest(final String digest) {
+ this.digest = digest;
+ return this;
+ }
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/service/ClusterSelectMasterService.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/service/ClusterSelectMasterService.java
new file mode 100644
index 000000000000..3b174a06d496
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/service/ClusterSelectMasterService.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.mode.cluster.service;
+
+import org.apache.shenyu.admin.model.dto.ClusterMasterDTO;
+
+public interface ClusterSelectMasterService {
+
+ /**
+ * Select the cluster master.
+ * @return select result
+ */
+ boolean selectMaster();
+
+ /**
+ * Select the cluster master.
+ * @param masterHost master host
+ * @param masterPort master port
+ * @param contextPath context path
+ * @return select result
+ */
+ boolean selectMaster(String masterHost, String masterPort, String contextPath);
+
+ /**
+ * check the cluster master status.
+ * @return check result
+ * @throws IllegalStateException Illegal State Exception
+ */
+ boolean checkMasterStatus() throws IllegalStateException;
+
+ /**
+ * Release the cluster master.
+ * @return release result
+ */
+ boolean releaseMaster();
+
+ /**
+ * Whether this node is the cluster master.
+ * @return is master
+ */
+ boolean isMaster();
+
+ /**
+ * Get master.
+ * @return ClusterMasterDTO
+ */
+ ClusterMasterDTO getMaster();
+
+ /**
+ * Get master url.
+ * @return master url
+ */
+ String getMasterUrl();
+
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/service/ShenyuClusterService.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/service/ShenyuClusterService.java
new file mode 100644
index 000000000000..c009d9e35b04
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/service/ShenyuClusterService.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.mode.cluster.service;
+
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.mode.ShenyuRunningModeService;
+import org.apache.shenyu.admin.service.impl.UpstreamCheckService;
+import org.apache.shenyu.admin.service.manager.LoadServiceDocEntry;
+import org.apache.shenyu.common.concurrent.ShenyuThreadFactory;
+import org.apache.shenyu.common.exception.ShenyuException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class ShenyuClusterService implements ShenyuRunningModeService {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ShenyuClusterService.class);
+
+ private final ClusterSelectMasterService shenyuClusterSelectMasterService;
+
+ private final UpstreamCheckService upstreamCheckService;
+
+ private final LoadServiceDocEntry loadServiceDocEntry;
+
+ private final ScheduledExecutorService executorService;
+
+ private final ClusterProperties clusterProperties;
+
+ public ShenyuClusterService(final ClusterSelectMasterService shenyuClusterSelectMasterService,
+ final UpstreamCheckService upstreamCheckService,
+ final LoadServiceDocEntry loadServiceDocEntry,
+ final ClusterProperties clusterProperties) {
+ this.shenyuClusterSelectMasterService = shenyuClusterSelectMasterService;
+ this.upstreamCheckService = upstreamCheckService;
+ this.loadServiceDocEntry = loadServiceDocEntry;
+ this.clusterProperties = clusterProperties;
+ this.executorService = new ScheduledThreadPoolExecutor(1,
+ ShenyuThreadFactory.create("master-selector", true));
+ }
+
+ /**
+ * start master select task.
+ *
+ * @param host host
+ * @param port port
+ * @param contextPath contextPath
+ */
+ public void startSelectMasterTask(final String host, final String port, final String contextPath) {
+ LOG.info("starting select master task");
+ // schedule task selectPeriod seconds
+ executorService.scheduleAtFixedRate(() -> doSelectMaster(host, port, contextPath),
+ 0,
+ clusterProperties.getSelectPeriod(),
+ TimeUnit.SECONDS);
+ }
+
+ private void doSelectMaster(final String host, final String port, final String contextPath) {
+ // try getting lock
+ try {
+ boolean selected = shenyuClusterSelectMasterService.selectMaster(host, port, contextPath);
+ if (!selected) {
+ LOG.info("select master fail, wait for next period");
+ return;
+ }
+
+ LOG.info("select master success");
+
+ // start upstream check task
+ upstreamCheckService.setup();
+
+ // load api
+ loadServiceDocEntry.loadApiDocument();
+
+ boolean renewed = shenyuClusterSelectMasterService.checkMasterStatus();
+
+ while (renewed) {
+ // sleeps selectPeriod seconds then renew the lock
+ TimeUnit.SECONDS.sleep(clusterProperties.getSelectPeriod());
+
+ renewed = shenyuClusterSelectMasterService.checkMasterStatus();
+ if (renewed) {
+ LOG.info("renew master success");
+ }
+ }
+ } catch (Exception e) {
+ LOG.error("select master error", e);
+ // close the upstream check service
+ upstreamCheckService.close();
+
+ String message = String.format("renew master fail, %s", e.getMessage());
+ throw new ShenyuException(message);
+ } finally {
+ try {
+ shenyuClusterSelectMasterService.releaseMaster();
+ } catch (Exception e) {
+ LOG.error("release master error", e);
+ }
+ }
+ }
+
+ @Override
+ public void start(final String host, final int port, final String contextPath) {
+ startSelectMasterTask(host, String.valueOf(port), contextPath);
+ }
+
+ @Override
+ public void shutdown() {
+
+ }
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/standalone/ShenyuStandaloneService.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/standalone/ShenyuStandaloneService.java
new file mode 100644
index 000000000000..ebb11c448892
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/standalone/ShenyuStandaloneService.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.mode.standalone;
+
+import org.apache.shenyu.admin.mode.ShenyuRunningModeService;
+import org.apache.shenyu.admin.service.impl.UpstreamCheckService;
+import org.apache.shenyu.admin.service.manager.LoadServiceDocEntry;
+
+public class ShenyuStandaloneService implements ShenyuRunningModeService {
+
+ private final UpstreamCheckService upstreamCheckService;
+
+ private final LoadServiceDocEntry loadServiceDocEntry;
+
+ public ShenyuStandaloneService(final UpstreamCheckService upstreamCheckService,
+ final LoadServiceDocEntry loadServiceDocEntry) {
+ this.upstreamCheckService = upstreamCheckService;
+ this.loadServiceDocEntry = loadServiceDocEntry;
+ }
+
+ @Override
+ public void start(final String host, final int port, final String contextPath) {
+ // start upstream check task
+ upstreamCheckService.setup();
+ // load api
+ loadServiceDocEntry.loadApiDocument();
+ }
+
+ @Override
+ public void shutdown() {
+ upstreamCheckService.close();
+ }
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/ClusterMasterDTO.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/ClusterMasterDTO.java
new file mode 100644
index 000000000000..51b5100bef36
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/ClusterMasterDTO.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.model.dto;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * The type cluster master dto.
+ */
+public class ClusterMasterDTO implements Serializable {
+
+ private static final long serialVersionUID = 803678746937608497L;
+
+ /**
+ * primary key id.
+ */
+ private String id;
+
+ /**
+ * the master host.
+ */
+ private String masterHost;
+
+ /**
+ * the master port.
+ */
+ private String masterPort;
+
+ /**
+ * the master contextPath.
+ */
+ private String contextPath;
+
+ /**
+ * create time.
+ */
+ private Date dateCreated;
+
+ /**
+ * update time.
+ */
+ private Date dateUpdated;
+
+ /**
+ * getId.
+ *
+ * @return id
+ */
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * set id.
+ *
+ * @param id id
+ */
+ public void setId(final String id) {
+ this.id = id;
+ }
+
+ /**
+ * Get master host.
+ * @return master host
+ */
+ public String getMasterHost() {
+ return masterHost;
+ }
+
+ /**
+ * Set master host.
+ * @param masterHost master host
+ */
+ public void setMasterHost(final String masterHost) {
+ this.masterHost = masterHost;
+ }
+
+ /**
+ * Get master port.
+ * @return master port
+ */
+ public String getMasterPort() {
+ return masterPort;
+ }
+
+ /**
+ * Set master port.
+ * @param masterPort master port
+ */
+ public void setMasterPort(final String masterPort) {
+ this.masterPort = masterPort;
+ }
+
+ /**
+ * Get context path.
+ * @return context path
+ */
+ public String getContextPath() {
+ return contextPath;
+ }
+
+ /**
+ * Set context path.
+ * @param contextPath context path
+ */
+ public void setContextPath(final String contextPath) {
+ this.contextPath = contextPath;
+ }
+
+ /**
+ * getDateCreated.
+ *
+ * @return dateCreated
+ */
+ public Date getDateCreated() {
+ return dateCreated;
+ }
+
+ /**
+ * setDateCreated.
+ *
+ * @param dateCreated dateCreated
+ */
+ public void setDateCreated(final Date dateCreated) {
+ this.dateCreated = dateCreated;
+ }
+
+ /**
+ * getDateUpdated.
+ *
+ * @return dateUpdated
+ */
+ public Date getDateUpdated() {
+ return dateUpdated;
+ }
+
+ /**
+ * setDateUpdated.
+ *
+ * @param dateUpdated dateUpdated
+ */
+ public void setDateUpdated(final Date dateUpdated) {
+ this.dateUpdated = dateUpdated;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ClusterMasterDTO)) {
+ return false;
+ }
+ ClusterMasterDTO apiDTO = (ClusterMasterDTO) o;
+ return Objects.equals(id, apiDTO.id)
+ && Objects.equals(masterHost, apiDTO.masterHost)
+ && Objects.equals(masterPort, apiDTO.masterPort)
+ && Objects.equals(dateCreated, apiDTO.dateCreated)
+ && Objects.equals(dateUpdated, apiDTO.dateUpdated);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, masterHost, masterPort, dateCreated, dateUpdated);
+ }
+
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/ClusterMasterDO.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/ClusterMasterDO.java
new file mode 100644
index 000000000000..e91f8c6522c0
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/entity/ClusterMasterDO.java
@@ -0,0 +1,303 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.model.entity;
+
+import java.io.Serializable;
+import java.sql.Timestamp;
+import java.util.Date;
+
+/**
+ * The type cluster master dto.
+ */
+public class ClusterMasterDO implements Serializable {
+
+ private static final long serialVersionUID = -7115137071311176280L;
+
+ /**
+ * primary key id.
+ */
+ private String id;
+
+ /**
+ * the masterHost.
+ */
+ private String masterHost;
+
+ /**
+ * the masterPort.
+ */
+ private String masterPort;
+
+ /**
+ * the contextPath.
+ */
+ private String contextPath;
+
+ /**
+ * created time.
+ */
+ private Timestamp dateCreated;
+
+ /**
+ * updated time.
+ */
+ private Timestamp dateUpdated;
+
+ public ClusterMasterDO() {
+ }
+
+ public ClusterMasterDO(final String id,
+ final String masterHost,
+ final String masterPort,
+ final Timestamp dateCreated,
+ final Timestamp dateUpdated) {
+ this.id = id;
+ this.masterHost = masterHost;
+ this.masterPort = masterPort;
+ this.dateCreated = dateCreated;
+ this.dateUpdated = dateUpdated;
+ }
+
+ /**
+ * getId.
+ *
+ * @return id
+ */
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * set id.
+ *
+ * @param id id
+ */
+ public void setId(final String id) {
+ this.id = id;
+ }
+
+ /**
+ * Get masterHost.
+ * @return masterHost
+ */
+ public String getMasterHost() {
+ return masterHost;
+ }
+
+ /**
+ * Set masterHost.
+ * @param masterHost masterHost
+ */
+ public void setMasterHost(final String masterHost) {
+ this.masterHost = masterHost;
+ }
+
+ /**
+ * Get master port.
+ * @return master port
+ */
+ public String getMasterPort() {
+ return masterPort;
+ }
+
+ /**
+ * Set master port.
+ * @param masterPort master port
+ */
+ public void setMasterPort(final String masterPort) {
+ this.masterPort = masterPort;
+ }
+
+ /**
+ * Get the contextPath.
+ * @return contextPath
+ */
+ public String getContextPath() {
+ return contextPath;
+ }
+
+ /**
+ * Set the contextPath.
+ * @param contextPath contextPath
+ */
+ public void setContextPath(final String contextPath) {
+ this.contextPath = contextPath;
+ }
+
+ /**
+ * getDateCreated.
+ *
+ * @return dateCreated
+ */
+ public Date getDateCreated() {
+ return dateCreated;
+ }
+
+ /**
+ * setDateCreated.
+ *
+ * @param dateCreated dateCreated
+ */
+ public void setDateCreated(final Timestamp dateCreated) {
+ this.dateCreated = dateCreated;
+ }
+
+ /**
+ * getDateUpdated.
+ *
+ * @return dateUpdated
+ */
+ public Timestamp getDateUpdated() {
+ return dateUpdated;
+ }
+
+ /**
+ * setDateUpdated.
+ *
+ * @param dateUpdated dateUpdated
+ */
+ public void setDateUpdated(final Timestamp dateUpdated) {
+ this.dateUpdated = dateUpdated;
+ }
+
+
+ /**
+ * builder method.
+ *
+ * @return builder object.
+ */
+ public static ClusterMasterDOBuilder builder() {
+ return new ClusterMasterDOBuilder();
+ }
+
+ public static final class ClusterMasterDOBuilder {
+
+ /**
+ * primary key id.
+ */
+ private String id;
+
+ /**
+ * the master host.
+ */
+ private String masterHost;
+
+ /**
+ * the master port.
+ */
+ private String masterPort;
+
+ /**
+ * the master contextPath.
+ */
+ private String contextPath;
+
+ /**
+ * created time.
+ */
+ private Timestamp dateCreated;
+
+ /**
+ * updated time.
+ */
+ private Timestamp dateUpdated;
+
+ /**
+ * id.
+ *
+ * @param id the id.
+ * @return ClusterMasterDOBuilder.
+ */
+ public ClusterMasterDOBuilder id(final String id) {
+ this.id = id;
+ return this;
+ }
+
+ /**
+ * master host.
+ *
+ * @param masterHost the master host.
+ * @return ClusterMasterDOBuilder.
+ */
+ public ClusterMasterDOBuilder masterHost(final String masterHost) {
+ this.masterHost = masterHost;
+ return this;
+ }
+
+ /**
+ * master port.
+ *
+ * @param masterPort the master port.
+ * @return ClusterMasterDOBuilder.
+ */
+ public ClusterMasterDOBuilder masterPort(final String masterPort) {
+ this.masterPort = masterPort;
+ return this;
+ }
+
+ /**
+ * master contextPath.
+ *
+ * @param contextPath the master contextPath.
+ * @return ClusterMasterDOBuilder.
+ */
+ public ClusterMasterDOBuilder contextPath(final String contextPath) {
+ this.contextPath = contextPath;
+ return this;
+ }
+
+ /**
+ * dateCreated.
+ *
+ * @param dateCreated the dateCreated.
+ * @return ClusterMasterDOBuilder.
+ */
+ public ClusterMasterDOBuilder dateCreated(final Timestamp dateCreated) {
+ this.dateCreated = dateCreated;
+ return this;
+ }
+
+ /**
+ * dateUpdated.
+ *
+ * @param dateUpdated the dateUpdated.
+ * @return ClusterMasterDOBuilder.
+ */
+ public ClusterMasterDOBuilder dateUpdated(final Timestamp dateUpdated) {
+ this.dateUpdated = dateUpdated;
+ return this;
+ }
+
+ /**
+ * build method.
+ *
+ * @return build object.
+ */
+ public ClusterMasterDO build() {
+ ClusterMasterDO clusterMasterDO = new ClusterMasterDO();
+ clusterMasterDO.setId(id);
+ clusterMasterDO.setMasterHost(masterHost);
+ clusterMasterDO.setMasterPort(masterPort);
+ clusterMasterDO.setContextPath(contextPath);
+ clusterMasterDO.setDateCreated(dateCreated);
+ clusterMasterDO.setDateUpdated(dateUpdated);
+ return clusterMasterDO;
+ }
+
+ }
+}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/PluginServiceImpl.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/PluginServiceImpl.java
index f3da88bc7543..ba02dba962de 100644
--- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/PluginServiceImpl.java
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/PluginServiceImpl.java
@@ -312,7 +312,7 @@ public ConfigImportResult importData(final List pluginList) {
* @see PluginCreatedEvent
*/
private String create(final PluginDTO pluginDTO) {
- Assert.isNull(pluginMapper.nameExisted(pluginDTO.getName()), AdminConstants.PLUGIN_NAME_IS_EXIST);
+ Assert.isNull(pluginMapper.nameExisted(pluginDTO.getName()), "create" + AdminConstants.PLUGIN_NAME_IS_EXIST + pluginDTO.getName());
if (Objects.nonNull(pluginDTO.getFile())) {
Assert.isTrue(checkFile(Base64.decode(pluginDTO.getFile())), AdminConstants.THE_PLUGIN_JAR_FILE_IS_NOT_CORRECT_OR_EXCEEDS_16_MB);
}
@@ -332,7 +332,7 @@ private String create(final PluginDTO pluginDTO) {
* @return success is empty
*/
private String update(final PluginDTO pluginDTO) {
- Assert.isNull(pluginMapper.nameExistedExclude(pluginDTO.getName(), Collections.singletonList(pluginDTO.getId())), AdminConstants.PLUGIN_NAME_IS_EXIST);
+ Assert.isNull(pluginMapper.nameExistedExclude(pluginDTO.getName(), Collections.singletonList(pluginDTO.getId())), AdminConstants.PLUGIN_NAME_IS_EXIST + pluginDTO.getName());
if (Objects.nonNull(pluginDTO.getFile())) {
Assert.isTrue(checkFile(Base64.decode(pluginDTO.getFile())), AdminConstants.THE_PLUGIN_JAR_FILE_IS_NOT_CORRECT_OR_EXCEEDS_16_MB);
}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/UpstreamCheckService.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/UpstreamCheckService.java
index 142cec04bb06..fdef9ef6e609 100644
--- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/UpstreamCheckService.java
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/UpstreamCheckService.java
@@ -147,16 +147,14 @@ public UpstreamCheckService(final SelectorMapper selectorMapper,
this.scheduledTime = Integer.parseInt(props.getProperty(Constants.SCHEDULED_TIME, Constants.SCHEDULED_TIME_VALUE));
this.registerType = shenyuRegisterCenterConfig.getRegisterType();
zombieRemovalTimes = Integer.parseInt(props.getProperty(Constants.ZOMBIE_REMOVAL_TIMES, Constants.ZOMBIE_REMOVAL_TIMES_VALUE));
- if (REGISTER_TYPE_HTTP.equalsIgnoreCase(registerType)) {
- setup();
- }
}
/**
* Set up.
*/
public void setup() {
- if (checked) {
+ if (REGISTER_TYPE_HTTP.equalsIgnoreCase(registerType) && checked) {
+ LOG.info("setup upstream check task");
this.fetchUpstreamData();
executor = new ScheduledThreadPoolExecutor(1, ShenyuThreadFactory.create("scheduled-upstream-task", false));
scheduledFuture = executor.scheduleWithFixedDelay(this::scheduled, 10, scheduledTime, TimeUnit.SECONDS);
@@ -172,8 +170,12 @@ public void setup() {
@PreDestroy
public void close() {
if (checked) {
- scheduledFuture.cancel(false);
- executor.shutdown();
+ if (Objects.nonNull(scheduledFuture)) {
+ scheduledFuture.cancel(false);
+ }
+ if (Objects.nonNull(executor)) {
+ executor.shutdown();
+ }
}
}
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/transfer/ClusterMasterTransfer.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/transfer/ClusterMasterTransfer.java
new file mode 100644
index 000000000000..41455e6fcada
--- /dev/null
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/transfer/ClusterMasterTransfer.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.transfer;
+
+import org.apache.shenyu.admin.model.dto.ClusterMasterDTO;
+import org.apache.shenyu.admin.model.entity.ClusterMasterDO;
+
+/**
+ * The interface Cluster Master transfer.
+ */
+public enum ClusterMasterTransfer {
+
+ /**
+ * The constant INSTANCE.
+ */
+ INSTANCE;
+
+ /**
+ * Map to entity meta data do.
+ *
+ * @param clusterMasterDO the meta data dto
+ * @return the meta data do
+ */
+ public ClusterMasterDTO mapToDTO(final ClusterMasterDO clusterMasterDO) {
+ ClusterMasterDTO clusterMasterDTO = new ClusterMasterDTO();
+ clusterMasterDTO.setId(clusterMasterDO.getId());
+ clusterMasterDTO.setMasterHost(clusterMasterDO.getMasterHost());
+ clusterMasterDTO.setMasterPort(clusterMasterDO.getMasterPort());
+ clusterMasterDTO.setContextPath(clusterMasterDO.getContextPath());
+ clusterMasterDTO.setDateCreated(clusterMasterDO.getDateCreated());
+ clusterMasterDTO.setDateUpdated(clusterMasterDO.getDateUpdated());
+ return clusterMasterDTO;
+ }
+
+}
diff --git a/shenyu-admin/src/main/resources/application.yml b/shenyu-admin/src/main/resources/application.yml
index 90bbc7bd1a2a..d764f459087e 100755
--- a/shenyu-admin/src/main/resources/application.yml
+++ b/shenyu-admin/src/main/resources/application.yml
@@ -112,6 +112,21 @@ shenyu:
login-field: cn
jwt:
expired-seconds: 86400000
+ cluster:
+ enabled: false
+ type: jdbc
+ forward-list:
+ - /shenyu-client/**
+ - /configs/**
+ - /selector/batchEnabled
+ - /selector/batch
+ - /rule/batchEnabled
+ - /rule/batch
+ - /plugin/syncPluginData/**
+ zookeeper:
+ url: localhost:2181
+ sessionTimeout: 5000
+ connectionTimeout: 2000
shiro:
white-list:
- /
diff --git a/shenyu-admin/src/main/resources/mappers/cluster-master-sqlmap.xml b/shenyu-admin/src/main/resources/mappers/cluster-master-sqlmap.xml
new file mode 100644
index 000000000000..2d7a0c2bc282
--- /dev/null
+++ b/shenyu-admin/src/main/resources/mappers/cluster-master-sqlmap.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id, master_host, master_port, context_path, date_created, date_updated
+
+
+
+ INSERT INTO cluster_master
+ (id,
+ master_host,
+ master_port,
+ context_path,
+ date_created,
+ date_updated)
+ VALUES
+ (#{id, jdbcType=VARCHAR},
+ #{masterHost, jdbcType=VARCHAR},
+ #{masterPort, jdbcType=VARCHAR},
+ #{contextPath, jdbcType=VARCHAR},
+ #{dateCreated, jdbcType=TIMESTAMP},
+ #{dateUpdated, jdbcType=TIMESTAMP})
+
+
+
+ UPDATE cluster_master
+
+
+ date_created = #{dateCreated, jdbcType=TIMESTAMP},
+
+
+ date_updated = #{dateUpdated, jdbcType=TIMESTAMP},
+
+
+ master_host = #{masterHost, jdbcType=VARCHAR},
+
+
+ master_port = #{masterPort, jdbcType=VARCHAR},
+
+
+ context_path = #{contextPath, jdbcType=VARCHAR},
+
+
+ WHERE id = #{id, jdbcType=VARCHAR}
+
+
+
+
+
+
diff --git a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
index 6f70e0407002..707835b8daec 100755
--- a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
+++ b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
@@ -1204,12 +1204,25 @@ CREATE TABLE IF NOT EXISTS `alert_receiver`
);
-- ----------------------------
--- Table structure for INT_LOCK
+-- Table structure for shenyu_lock
-- ----------------------------
-CREATE TABLE IF NOT EXISTS `INT_LOCK` (
+CREATE TABLE IF NOT EXISTS `SHENYU_LOCK` (
`LOCK_KEY` CHAR(36),
`REGION` VARCHAR(100),
`CLIENT_ID` CHAR(36),
`CREATED_DATE` TIMESTAMP NOT NULL,
- constraint INT_LOCK_PK primary key (LOCK_KEY, REGION)
+ constraint SHENYU_LOCK primary key (LOCK_KEY, REGION)
);
+
+-- ----------------------------
+-- Table structure for cluster_master
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS cluster_master (
+ `id` varchar(128) NOT NULL COMMENT 'primary key id',
+ `master_host` varchar(255) COMMENT 'master host',
+ `master_port` varchar(255) COMMENT 'master port',
+ `context_path` varchar(255) COMMENT 'master context_path',
+ `date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'create time',
+ `date_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT 'update time',
+ PRIMARY KEY (`id`)
+) ;
diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/DataChangedEventDispatcherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/DataChangedEventDispatcherTest.java
index 9d72d2400f65..190f44536f59 100644
--- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/DataChangedEventDispatcherTest.java
+++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/DataChangedEventDispatcherTest.java
@@ -17,10 +17,12 @@
package org.apache.shenyu.admin.listener;
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
import org.apache.shenyu.admin.listener.http.HttpLongPollingDataChangedListener;
import org.apache.shenyu.admin.listener.nacos.NacosDataChangedListener;
import org.apache.shenyu.admin.listener.websocket.WebsocketDataChangedListener;
import org.apache.shenyu.admin.listener.zookeeper.ZookeeperDataChangedListener;
+import org.apache.shenyu.admin.mode.cluster.service.ClusterSelectMasterService;
import org.apache.shenyu.admin.service.manager.LoadServiceDocEntry;
import org.apache.shenyu.common.enums.ConfigGroupEnum;
import org.junit.jupiter.api.BeforeEach;
@@ -32,6 +34,7 @@
import org.springframework.context.ApplicationContext;
import org.springframework.test.util.ReflectionTestUtils;
+import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -72,9 +75,15 @@ public final class DataChangedEventDispatcherTest {
@Mock
private LoadServiceDocEntry loadServiceDocEntry;
+
+ @Mock
+ private ClusterProperties clusterProperties;
+
+ @Mock
+ private ClusterSelectMasterService shenyuClusterSelectMasterService;
@BeforeEach
- public void setUp() {
+ public void setUp() throws NoSuchFieldException, IllegalAccessException {
Map listenerMap = new HashMap<>();
listenerMap.put("httpLongPollingDataChangedListener", httpLongPollingDataChangedListener);
listenerMap.put("nacosDataChangedListener", nacosDataChangedListener);
@@ -84,6 +93,17 @@ public void setUp() {
when(applicationContext.getBean(LoadServiceDocEntry.class)).thenReturn(loadServiceDocEntry);
applicationContext.getBean(LoadServiceDocEntry.class);
+
+ Field shenyuClusterSelectMasterServiceField = DataChangedEventDispatcher.class.getDeclaredField("shenyuClusterSelectMasterService");
+ shenyuClusterSelectMasterServiceField.setAccessible(true);
+ shenyuClusterSelectMasterServiceField.set(dataChangedEventDispatcher, shenyuClusterSelectMasterService);
+ shenyuClusterSelectMasterServiceField.setAccessible(false);
+
+ Field clusterPropertiesField = DataChangedEventDispatcher.class.getDeclaredField("clusterProperties");
+ clusterPropertiesField.setAccessible(true);
+ clusterPropertiesField.set(dataChangedEventDispatcher, clusterProperties);
+ clusterPropertiesField.setAccessible(false);
+
dataChangedEventDispatcher.afterPropertiesSet();
}
@@ -92,6 +112,8 @@ public void setUp() {
*/
@Test
public void onApplicationEventWithAppAuthConfigGroupTest() {
+ when(clusterProperties.isEnabled()).thenReturn(true);
+ when(shenyuClusterSelectMasterService.isMaster()).thenReturn(true);
ConfigGroupEnum configGroupEnum = ConfigGroupEnum.APP_AUTH;
DataChangedEvent dataChangedEvent = new DataChangedEvent(configGroupEnum, null, new ArrayList<>());
dataChangedEventDispatcher.onApplicationEvent(dataChangedEvent);
@@ -106,6 +128,8 @@ public void onApplicationEventWithAppAuthConfigGroupTest() {
*/
@Test
public void onApplicationEventWithPluginConfigGroupTest() {
+ when(clusterProperties.isEnabled()).thenReturn(true);
+ when(shenyuClusterSelectMasterService.isMaster()).thenReturn(true);
ConfigGroupEnum configGroupEnum = ConfigGroupEnum.PLUGIN;
DataChangedEvent dataChangedEvent = new DataChangedEvent(configGroupEnum, null, new ArrayList<>());
dataChangedEventDispatcher.onApplicationEvent(dataChangedEvent);
@@ -120,6 +144,8 @@ public void onApplicationEventWithPluginConfigGroupTest() {
*/
@Test
public void onApplicationEventWithRuleConfigGroupTest() {
+ when(clusterProperties.isEnabled()).thenReturn(true);
+ when(shenyuClusterSelectMasterService.isMaster()).thenReturn(true);
ConfigGroupEnum configGroupEnum = ConfigGroupEnum.RULE;
DataChangedEvent dataChangedEvent = new DataChangedEvent(configGroupEnum, null, new ArrayList<>());
dataChangedEventDispatcher.onApplicationEvent(dataChangedEvent);
@@ -134,6 +160,8 @@ public void onApplicationEventWithRuleConfigGroupTest() {
*/
@Test
public void onApplicationEventWithSelectorConfigGroupTest() {
+ when(clusterProperties.isEnabled()).thenReturn(true);
+ when(shenyuClusterSelectMasterService.isMaster()).thenReturn(true);
ConfigGroupEnum configGroupEnum = ConfigGroupEnum.SELECTOR;
DataChangedEvent dataChangedEvent = new DataChangedEvent(configGroupEnum, null, new ArrayList<>());
dataChangedEventDispatcher.onApplicationEvent(dataChangedEvent);
@@ -148,6 +176,8 @@ public void onApplicationEventWithSelectorConfigGroupTest() {
*/
@Test
public void onApplicationEventWithMetaDataConfigGroupTest() {
+ when(clusterProperties.isEnabled()).thenReturn(true);
+ when(shenyuClusterSelectMasterService.isMaster()).thenReturn(true);
ConfigGroupEnum configGroupEnum = ConfigGroupEnum.META_DATA;
DataChangedEvent dataChangedEvent = new DataChangedEvent(configGroupEnum, null, new ArrayList<>());
dataChangedEventDispatcher.onApplicationEvent(dataChangedEvent);
@@ -162,6 +192,8 @@ public void onApplicationEventWithMetaDataConfigGroupTest() {
*/
@Test
public void onApplicationEventWithNullTest() {
+ when(clusterProperties.isEnabled()).thenReturn(true);
+ when(shenyuClusterSelectMasterService.isMaster()).thenReturn(true);
NullPointerException exception = assertThrows(NullPointerException.class, () -> {
DataChangedEvent dataChangedEvent = new DataChangedEvent(null, null, new ArrayList<>());
dataChangedEventDispatcher.onApplicationEvent(dataChangedEvent);
diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketCollectorTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketCollectorTest.java
index c0777fd123a3..176a21a22f8b 100644
--- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketCollectorTest.java
+++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketCollectorTest.java
@@ -138,6 +138,7 @@ public void testOnError() {
public void testSend() throws IOException {
RemoteEndpoint.Basic basic = mock(RemoteEndpoint.Basic.class);
when(session.getBasicRemote()).thenReturn(basic);
+ when(session.isOpen()).thenReturn(true);
websocketCollector.onOpen(session);
assertEquals(1L, getSessionSetSize());
WebsocketCollector.send(null, DataEventTypeEnum.MYSELF);
diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ClusterSelectMasterServiceJdbcImplTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ClusterSelectMasterServiceJdbcImplTest.java
new file mode 100644
index 000000000000..104e122bc808
--- /dev/null
+++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ClusterSelectMasterServiceJdbcImplTest.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service;
+
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.mode.cluster.impl.jdbc.ClusterSelectMasterServiceJdbcImpl;
+import org.apache.shenyu.admin.mode.cluster.impl.jdbc.mapper.ClusterMasterMapper;
+import org.apache.shenyu.admin.model.dto.ClusterMasterDTO;
+import org.apache.shenyu.admin.model.entity.ClusterMasterDO;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.integration.jdbc.lock.JdbcLockRegistry;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.locks.Lock;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Test cases for ClusterSelectMasterServiceJdbcImpl.
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public final class ClusterSelectMasterServiceJdbcImplTest {
+
+ private static final String CONTEXT_PATH = "/test";
+
+ private static final String PORT = "8080";
+
+ private static final String HOST = "127.0.0.1";
+
+ @InjectMocks
+ private ClusterSelectMasterServiceJdbcImpl clusterSelectMasterServiceJdbc;
+
+ @Mock
+ private ClusterProperties clusterProperties;
+
+ @Mock
+ private JdbcLockRegistry jdbcLockRegistry;
+
+ @Mock
+ private Lock clusterMasterLock;
+
+ @Mock
+ private ClusterMasterMapper clusterMasterMapper;
+
+ @BeforeEach
+ public void setUp() throws NoSuchFieldException, IllegalAccessException {
+ clusterSelectMasterServiceJdbc = new ClusterSelectMasterServiceJdbcImpl(clusterProperties, jdbcLockRegistry, clusterMasterMapper);
+ given(clusterMasterLock.tryLock()).willReturn(true);
+ Field clusterMasterLockField = ClusterSelectMasterServiceJdbcImpl.class.getDeclaredField("clusterMasterLock");
+ clusterMasterLockField.setAccessible(true);
+ clusterMasterLockField.set(clusterSelectMasterServiceJdbc, clusterMasterLock);
+ }
+
+ @Test
+ void testSetMaster() {
+
+ given(clusterMasterMapper.insert(any())).willReturn(1);
+
+ clusterSelectMasterServiceJdbc.selectMaster(HOST, PORT, CONTEXT_PATH);
+
+ verify(clusterMasterMapper, times(1)).insert(any());
+ }
+
+ @Test
+ void testGetMaster() {
+
+ ClusterMasterDO clusterMasterDO = buildClusterMasterDO();
+
+ given(clusterMasterMapper.selectById(any())).willReturn(clusterMasterDO);
+
+ ClusterMasterDTO actual = clusterSelectMasterServiceJdbc.getMaster();
+
+ assertEquals(HOST, actual.getMasterHost());
+ assertEquals(PORT, actual.getMasterPort());
+ assertEquals(CONTEXT_PATH, actual.getContextPath());
+
+ }
+
+ private ClusterMasterDO buildClusterMasterDO() {
+ ClusterMasterDO clusterMasterDO = new ClusterMasterDO();
+ clusterMasterDO.setId("1");
+ clusterMasterDO.setMasterHost(HOST);
+ clusterMasterDO.setMasterPort(PORT);
+ clusterMasterDO.setContextPath(CONTEXT_PATH);
+ return clusterMasterDO;
+ }
+}
diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/constant/AdminConstants.java b/shenyu-common/src/main/java/org/apache/shenyu/common/constant/AdminConstants.java
index 5db18edb8aed..d3316a25734c 100644
--- a/shenyu-common/src/main/java/org/apache/shenyu/common/constant/AdminConstants.java
+++ b/shenyu-common/src/main/java/org/apache/shenyu/common/constant/AdminConstants.java
@@ -275,5 +275,9 @@ public final class AdminConstants {
public static final String PROXY_SELECTOR_ID_IS_NOT_EXIST = "The proxy selector(s) does not exist";
public static final long THE_ONE_DAY_MILLIS_TIME = 24 * 60 * 60 * 1000L;
+
+ public static final long FIVE_SECONDS_MILLIS_TIME = 5 * 1000L;
+
+ public static final long TEN_SECONDS_MILLIS_TIME = 10 * 1000L;
}
diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/constant/RunningModeConstants.java b/shenyu-common/src/main/java/org/apache/shenyu/common/constant/RunningModeConstants.java
new file mode 100644
index 000000000000..6f1f4cc1ceac
--- /dev/null
+++ b/shenyu-common/src/main/java/org/apache/shenyu/common/constant/RunningModeConstants.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.common.constant;
+
+/**
+ * running mode constants.
+ */
+public class RunningModeConstants {
+
+ /**
+ * The constant eventType.
+ */
+ public static final String EVENT_TYPE = "eventType";
+
+ /**
+ * The constant runningMode.
+ */
+ public static final String RUNNING_MODE = "runningMode";
+
+ /**
+ * The constant masterUrl.
+ */
+ public static final String MASTER_URL = "masterUrl";
+
+ /**
+ * The constant isMaster.
+ */
+ public static final String IS_MASTER = "isMaster";
+
+ /**
+ * The constant CLUSTER.
+ */
+ public static final String CLUSTER = "CLUSTER";
+}
diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/enums/DataEventTypeEnum.java b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/DataEventTypeEnum.java
index 6c54a5c71be6..39905c433036 100644
--- a/shenyu-common/src/main/java/org/apache/shenyu/common/enums/DataEventTypeEnum.java
+++ b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/DataEventTypeEnum.java
@@ -47,6 +47,11 @@ public enum DataEventTypeEnum {
*/
REFRESH,
+ /**
+ * RUNNING_MODE data event type enum.
+ */
+ RUNNING_MODE,
+
/**
* Myself data event type enum.
*/
diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/enums/RunningModeEnum.java b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/RunningModeEnum.java
new file mode 100644
index 000000000000..a388cca7cda7
--- /dev/null
+++ b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/RunningModeEnum.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.common.enums;
+
+/**
+ * the running mode.
+ */
+public enum RunningModeEnum {
+
+ /**
+ * standalone.
+ */
+ STANDALONE,
+
+ /**
+ * cluster.
+ */
+ CLUSTER;
+
+}
diff --git a/shenyu-dist/shenyu-bootstrap-dist/docker/Dockerfile b/shenyu-dist/shenyu-bootstrap-dist/docker/Dockerfile
index 72fee6f805c5..03cf11c78377 100644
--- a/shenyu-dist/shenyu-bootstrap-dist/docker/Dockerfile
+++ b/shenyu-dist/shenyu-bootstrap-dist/docker/Dockerfile
@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-FROM alpine AS prepare
+FROM centos:7 AS prepare
ARG APP_NAME
@@ -23,9 +23,10 @@ ENV LOCAL_PATH /opt/shenyu-bootstrap
ADD target/${APP_NAME}.tar.gz /opt
RUN mv /opt/${APP_NAME} ${LOCAL_PATH}
-FROM amazoncorretto:17.0.11-alpine3.19
+# FROM amazoncorretto:17.0.11-alpine3.19
+FROM eclipse-temurin:17-centos7
-RUN apk --no-cache add wget curl
+# RUN apk --no-cache add wget curl
ENV LOCAL_PATH /opt/shenyu-bootstrap
ENV BOOT_JVM ""
diff --git a/shenyu-e2e/shenyu-e2e-case/pom.xml b/shenyu-e2e/shenyu-e2e-case/pom.xml
index baba0555cf78..901a5c593615 100644
--- a/shenyu-e2e/shenyu-e2e-case/pom.xml
+++ b/shenyu-e2e/shenyu-e2e-case/pom.xml
@@ -29,6 +29,7 @@
pom
+ shenyu-e2e-case-cluster
shenyu-e2e-case-storage
shenyu-e2e-case-http
shenyu-e2e-case-spring-cloud
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-apache-dubbo/k8s/script/e2e-apache-dubbo-sync.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-apache-dubbo/k8s/script/e2e-apache-dubbo-sync.sh
index ddf29d5e6050..9940fba62f80 100644
--- a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-apache-dubbo/k8s/script/e2e-apache-dubbo-sync.sh
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-apache-dubbo/k8s/script/e2e-apache-dubbo-sync.sh
@@ -58,6 +58,9 @@ for sync in ${SYNC_ARRAY[@]}; do
# shellcheck disable=SC2181
if (($?)); then
echo "${sync}-sync-e2e-test failed"
+ echo "shenyu-examples-dubbo-deployment log:"
+ echo "------------------"
+ kubectl logs "$(kubectl get pod -o wide | grep shenyu-examples-dubbo-deployment | awk '{print $1}')"
echo "shenyu-admin log:"
echo "------------------"
kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin | awk '{print $1}')"
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/script/e2e-cluster-jdbc.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/script/e2e-cluster-jdbc.sh
new file mode 100644
index 000000000000..f71a72c4fb3f
--- /dev/null
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/script/e2e-cluster-jdbc.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# init kubernetes for mysql
+shenyuTestCaseDir=$(dirname "$(dirname "$(dirname "$(dirname "$0")")")")
+echo "$shenyuTestCaseDir"
+bash "$shenyuTestCaseDir"/k8s/script/init/mysql_container_init.sh
+
+curPath=$(readlink -f "$(dirname "$0")")
+PRGDIR=$(dirname "$curPath")
+echo "$PRGDIR"
+kubectl apply -f "${PRGDIR}"/shenyu-cluster-jdbc.yml
+
+kubectl get pod -o wide
+
+sleep 30s
+
+chmod +x "${curPath}"/healthcheck.sh
+sh "${curPath}"/healthcheck.sh cluster http://localhost:31095/actuator/health http://localhost:31096/actuator/health http://localhost:31195/actuator/health
+
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin-master | awk '{print $1}')"
+
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin-slave | awk '{print $1}')"
+
+kubectl describe pod shenyu-admin-master
+
+kubectl describe pod shenyu-admin-slave
+
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-bootstrap | awk '{print $1}')"
+
+## run e2e-test
+sleep 60s
+curl -S "http://localhost:31195/actuator/pluginData"
+
+./mvnw -B -f ./shenyu-e2e/pom.xml -pl shenyu-e2e-case/shenyu-e2e-case-cluster -am test
+
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/script/e2e-cluster-zookeeper.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/script/e2e-cluster-zookeeper.sh
new file mode 100644
index 000000000000..01cb6ce50fd1
--- /dev/null
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/script/e2e-cluster-zookeeper.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# init kubernetes for mysql
+shenyuTestCaseDir=$(dirname "$(dirname "$(dirname "$(dirname "$0")")")")
+echo "$shenyuTestCaseDir"
+bash "$shenyuTestCaseDir"/k8s/script/init/mysql_container_init.sh
+
+curPath=$(readlink -f "$(dirname "$0")")
+PRGDIR=$(dirname "$curPath")
+echo "$PRGDIR"
+kubectl apply -f "${shenyuTestCaseDir}"/k8s/shenyu-zookeeper.yml
+kubectl apply -f "${PRGDIR}"/shenyu-cluster-zookeeper.yml
+
+kubectl get pod -o wide
+
+sleep 30s
+
+chmod +x "${curPath}"/healthcheck.sh
+sh "${curPath}"/healthcheck.sh cluster http://localhost:31095/actuator/health http://localhost:31096/actuator/health http://localhost:31195/actuator/health
+
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-mysql | awk '{print $1}')"
+
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin-master | awk '{print $1}')"
+
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin-slave | awk '{print $1}')"
+
+kubectl describe pod shenyu-admin-master
+
+kubectl describe pod shenyu-admin-slave
+
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-bootstrap | awk '{print $1}')"
+
+## run e2e-test
+sleep 60s
+curl -S "http://localhost:31195/actuator/pluginData"
+
+./mvnw -B -f ./shenyu-e2e/pom.xml -pl shenyu-e2e-case/shenyu-e2e-case-cluster -am test
+
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/script/healthcheck.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/script/healthcheck.sh
new file mode 100644
index 000000000000..7dbb32189efc
--- /dev/null
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/script/healthcheck.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+PRGDIR=$(dirname "$0")
+
+for service in $(grep -v -E "^$|^#" "${PRGDIR}"/services-"${1}".list)
+do
+ for loop in $(seq 1 30)
+ do
+ status=$(curl -o /dev/null -s -w %{http_code} "$service")
+ echo -e "curl $service response $status"
+
+ if [ "$status" -eq 200 ]; then
+ break
+ fi
+
+ sleep 2
+ done
+done
+
+
+admin_status=$(curl -s -o /dev/null -w "%{http_code}" -X GET "${2}" -H "accept: */*")
+bootstrap_status=$(curl -s -o /dev/null -w "%{http_code}" -X GET "${3}" -H "accept: */*")
+
+if [ "$admin_status" -eq 200 -a "$bootstrap_status" -eq 200 ]; then
+ echo -e "\n-------------------"
+ echo -e "Success to send request: $admin_status"
+ echo -e "Success to send request: $bootstrap_status"
+ echo -e "\n-------------------"
+ exit 0
+fi
+echo -e "\n-------------------"
+echo -e "Failed to send request from shenyu-admin : $admin_status"
+echo -e "Failed to send request from shenyu-bootstrap : $bootstrap_status"
+echo -e "\n-------------------"
+exit 1
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/script/services-cluster.list b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/script/services-cluster.list
new file mode 100644
index 000000000000..8f89453571c0
--- /dev/null
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/script/services-cluster.list
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+http://localhost:31095/actuator/health
+http://localhost:31096/actuator/health
+http://localhost:31195/actuator/health
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/shenyu-cluster-jdbc.yml b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/shenyu-cluster-jdbc.yml
new file mode 100644
index 000000000000..ffca44b582ab
--- /dev/null
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/shenyu-cluster-jdbc.yml
@@ -0,0 +1,249 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: shenyu-admin-master
+ labels:
+ app: shenyu-admin-master
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: shenyu-admin-master
+ template:
+ metadata:
+ labels:
+ app: shenyu-admin-master
+ spec:
+ containers:
+ - name: shenyu-admin-master
+ image: apache/shenyu-admin:latest
+ args:
+ - -Xmx768m -Xms768m
+ env:
+ - name: 'TZ'
+ value: 'Asia/Beijing'
+ - name: SPRING_PROFILES_ACTIVE
+ value: mysql
+ - name: server.port
+ value: "9095"
+ - name: shenyu.cluster.enabled
+ value: "true"
+ - name: spring.datasource.username
+ value: root
+ - name: spring.datasource.password
+ value: shenyue2e
+ - name: spring.datasource.url
+ value: jdbc:mysql://shenyu-mysql:3306/shenyu?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull
+ livenessProbe:
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ successThreshold: 1
+ failureThreshold: 3
+ httpGet:
+ port: 9095
+ path: /actuator/health
+ readinessProbe:
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ successThreshold: 1
+ failureThreshold: 3
+ httpGet:
+ port: 9095
+ path: /actuator/health
+ ports:
+ - containerPort: 9095
+ imagePullPolicy: IfNotPresent
+ volumeMounts:
+ - name: mysql-connector-volume
+ mountPath: /opt/shenyu-admin/ext-lib
+ volumes:
+ - name: mysql-connector-volume
+ hostPath:
+ path: /tmp/shenyu-e2e/mysql/driver
+ restartPolicy: Always
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: shenyu-admin-slave
+ labels:
+ app: shenyu-admin-slave
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: shenyu-admin-slave
+ template:
+ metadata:
+ labels:
+ app: shenyu-admin-slave
+ spec:
+ containers:
+ - name: shenyu-admin-slave
+ image: apache/shenyu-admin:latest
+ args:
+ - -Xmx768m -Xms768m
+ env:
+ - name: 'TZ'
+ value: 'Asia/Beijing'
+ - name: SPRING_PROFILES_ACTIVE
+ value: mysql
+ - name: server.port
+ value: "9096"
+ - name: shenyu.cluster.enabled
+ value: "true"
+ - name: spring.datasource.username
+ value: root
+ - name: spring.datasource.password
+ value: shenyue2e
+ - name: spring.datasource.url
+ value: jdbc:mysql://shenyu-mysql:3306/shenyu?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull
+ livenessProbe:
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ successThreshold: 1
+ failureThreshold: 3
+ httpGet:
+ port: 9096
+ path: /actuator/health
+ readinessProbe:
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ successThreshold: 1
+ failureThreshold: 3
+ httpGet:
+ port: 9096
+ path: /actuator/health
+ ports:
+ - containerPort: 9096
+ imagePullPolicy: IfNotPresent
+ volumeMounts:
+ - name: mysql-connector-volume
+ mountPath: /opt/shenyu-admin/ext-lib
+ volumes:
+ - name: mysql-connector-volume
+ hostPath:
+ path: /tmp/shenyu-e2e/mysql/driver
+ restartPolicy: Always
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: shenyu-bootstrap-cluster
+ labels:
+ app: shenyu-bootstrap-cluster
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: shenyu-bootstrap-cluster
+ template:
+ metadata:
+ labels:
+ app: shenyu-bootstrap-cluster
+ spec:
+ containers:
+ - name: shenyu-bootstrap-cluster
+ image: apache/shenyu-bootstrap:latest
+ resources: { }
+ args:
+ - -Xmx768m -Xms768m
+ env:
+ - name: shenyu.sync.websocket.urls
+ value: ws://shenyu-admin-master:9095/websocket,ws://shenyu-admin-slave:9096/websocket
+ ports:
+ - containerPort: 9195
+ livenessProbe:
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ successThreshold: 1
+ failureThreshold: 3
+ httpGet:
+ port: 9195
+ path: /actuator/health
+ readinessProbe:
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ successThreshold: 1
+ failureThreshold: 3
+ httpGet:
+ port: 9195
+ path: /actuator/health
+ imagePullPolicy: IfNotPresent
+ restartPolicy: Always
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: shenyu-admin-master
+ labels:
+ app: shenyu-admin-master
+spec:
+ type: NodePort
+ selector:
+ app: shenyu-admin-master
+ ports:
+ - name: "9095"
+ port: 9095
+ targetPort: 9095
+ nodePort: 31095
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: shenyu-admin-slave
+ labels:
+ app: shenyu-admin-slave
+spec:
+ type: NodePort
+ selector:
+ app: shenyu-admin-slave
+ ports:
+ - name: "9096"
+ port: 9096
+ targetPort: 9096
+ nodePort: 31096
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: shenyu-bootstrap-cluster
+ labels:
+ app: shenyu-bootstrap-cluster
+spec:
+ type: NodePort
+ selector:
+ app: shenyu-bootstrap-cluster
+ ports:
+ - name: "9195"
+ port: 9195
+ targetPort: 9195
+ nodePort: 31195
\ No newline at end of file
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/shenyu-cluster-zookeeper.yml b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/shenyu-cluster-zookeeper.yml
new file mode 100644
index 000000000000..bd4960973e7c
--- /dev/null
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/k8s/shenyu-cluster-zookeeper.yml
@@ -0,0 +1,256 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: shenyu-admin-master
+ labels:
+ app: shenyu-admin-master
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: shenyu-admin-master
+ template:
+ metadata:
+ labels:
+ app: shenyu-admin-master
+ spec:
+ containers:
+ - name: shenyu-admin-master
+ image: apache/shenyu-admin:latest
+ args:
+ - -Xmx768m -Xms768m
+ env:
+ - name: 'TZ'
+ value: 'Asia/Beijing'
+ - name: SPRING_PROFILES_ACTIVE
+ value: mysql
+ - name: server.port
+ value: "9095"
+ - name: shenyu.cluster.enabled
+ value: "true"
+ - name: shenyu.cluster.type
+ value: "zookeeper"
+ - name: shenyu.cluster.zookeeper.url
+ value: "shenyu-zookeeper:2181"
+ - name: spring.datasource.username
+ value: root
+ - name: spring.datasource.password
+ value: shenyue2e
+ - name: spring.datasource.url
+ value: jdbc:mysql://shenyu-mysql:3306/shenyu?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull
+ livenessProbe:
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ successThreshold: 1
+ failureThreshold: 3
+ httpGet:
+ port: 9095
+ path: /actuator/health
+ readinessProbe:
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ successThreshold: 1
+ failureThreshold: 3
+ httpGet:
+ port: 9095
+ path: /actuator/health
+ ports:
+ - containerPort: 9095
+ imagePullPolicy: IfNotPresent
+ volumeMounts:
+ - name: mysql-connector-volume
+ mountPath: /opt/shenyu-admin/ext-lib
+ volumes:
+ - name: mysql-connector-volume
+ hostPath:
+ path: /tmp/shenyu-e2e/mysql/driver
+ restartPolicy: Always
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: shenyu-admin-slave
+ labels:
+ app: shenyu-admin-slave
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: shenyu-admin-slave
+ template:
+ metadata:
+ labels:
+ app: shenyu-admin-slave
+ spec:
+ containers:
+ - name: shenyu-admin-slave
+ image: apache/shenyu-admin:latest
+ args:
+ - -Xmx768m -Xms768m
+ env:
+ - name: 'TZ'
+ value: 'Asia/Beijing'
+ - name: SPRING_PROFILES_ACTIVE
+ value: mysql
+ - name: server.port
+ value: "9096"
+ - name: shenyu.cluster.enabled
+ value: "true"
+ - name: shenyu.cluster.type
+ value: "zookeeper"
+ - name: shenyu.cluster.zookeeper.url
+ value: "shenyu-zookeeper:2181"
+ - name: spring.datasource.username
+ value: root
+ - name: spring.datasource.password
+ value: shenyue2e
+ - name: spring.datasource.url
+ value: jdbc:mysql://shenyu-mysql:3306/shenyu?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull
+ livenessProbe:
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ successThreshold: 1
+ failureThreshold: 3
+ httpGet:
+ port: 9096
+ path: /actuator/health
+ readinessProbe:
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ successThreshold: 1
+ failureThreshold: 3
+ httpGet:
+ port: 9096
+ path: /actuator/health
+ ports:
+ - containerPort: 9096
+ imagePullPolicy: IfNotPresent
+ volumeMounts:
+ - name: mysql-connector-volume
+ mountPath: /opt/shenyu-admin/ext-lib
+ volumes:
+ - name: mysql-connector-volume
+ hostPath:
+ path: /tmp/shenyu-e2e/mysql/driver
+ restartPolicy: Always
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: shenyu-bootstrap-cluster
+ labels:
+ app: shenyu-bootstrap-cluster
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: shenyu-bootstrap-cluster
+ template:
+ metadata:
+ labels:
+ app: shenyu-bootstrap-cluster
+ spec:
+ containers:
+ - name: shenyu-bootstrap-cluster
+ image: apache/shenyu-bootstrap:latest
+ resources: { }
+ args:
+ - -Xmx768m -Xms768m
+ env:
+ - name: shenyu.sync.websocket.urls
+ value: ws://shenyu-admin-master:9095/websocket,ws://shenyu-admin-slave:9096/websocket
+ ports:
+ - containerPort: 9195
+ livenessProbe:
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ successThreshold: 1
+ failureThreshold: 3
+ httpGet:
+ port: 9195
+ path: /actuator/health
+ readinessProbe:
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ successThreshold: 1
+ failureThreshold: 3
+ httpGet:
+ port: 9195
+ path: /actuator/health
+ imagePullPolicy: IfNotPresent
+ restartPolicy: Always
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: shenyu-admin-master
+ labels:
+ app: shenyu-admin-master
+spec:
+ type: NodePort
+ selector:
+ app: shenyu-admin-master
+ ports:
+ - name: "9095"
+ port: 9095
+ targetPort: 9095
+ nodePort: 31095
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: shenyu-admin-slave
+ labels:
+ app: shenyu-admin-slave
+spec:
+ type: NodePort
+ selector:
+ app: shenyu-admin-slave
+ ports:
+ - name: "9096"
+ port: 9096
+ targetPort: 9096
+ nodePort: 31096
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: shenyu-bootstrap-cluster
+ labels:
+ app: shenyu-bootstrap-cluster
+spec:
+ type: NodePort
+ selector:
+ app: shenyu-bootstrap-cluster
+ ports:
+ - name: "9195"
+ port: 9195
+ targetPort: 9195
+ nodePort: 31195
\ No newline at end of file
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/pom.xml b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/pom.xml
new file mode 100644
index 000000000000..c52ad596d9a6
--- /dev/null
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/pom.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ org.apache.shenyu
+ shenyu-e2e-case
+ 0.0.1-SNAPSHOT
+
+
+ 4.0.0
+
+ shenyu-e2e-case-cluster
+
+
+
\ No newline at end of file
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/src/test/java/org/apache/shenyue/e2e/testcase/cluster/DividePluginCases.java b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/src/test/java/org/apache/shenyue/e2e/testcase/cluster/DividePluginCases.java
new file mode 100644
index 000000000000..e91ab95dc8cc
--- /dev/null
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/src/test/java/org/apache/shenyue/e2e/testcase/cluster/DividePluginCases.java
@@ -0,0 +1,390 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyue.e2e.testcase.cluster;
+
+import com.google.common.collect.Lists;
+import io.restassured.http.Method;
+import org.apache.shenyu.e2e.engine.scenario.ShenYuScenarioProvider;
+import org.apache.shenyu.e2e.engine.scenario.specification.ScenarioSpec;
+import org.apache.shenyu.e2e.engine.scenario.specification.ShenYuAfterEachSpec;
+import org.apache.shenyu.e2e.engine.scenario.specification.ShenYuBeforeEachSpec;
+import org.apache.shenyu.e2e.engine.scenario.specification.ShenYuCaseSpec;
+import org.apache.shenyu.e2e.engine.scenario.specification.ShenYuScenarioSpec;
+import org.apache.shenyu.e2e.model.Plugin;
+import org.apache.shenyu.e2e.model.data.Condition.Operator;
+import org.apache.shenyu.e2e.model.data.Condition.ParamType;
+
+import java.util.List;
+
+import static org.apache.shenyu.e2e.engine.scenario.function.HttpCheckers.exists;
+import static org.apache.shenyu.e2e.engine.scenario.function.HttpCheckers.notExists;
+import static org.apache.shenyu.e2e.template.ResourceDataTemplate.newBindingData;
+import static org.apache.shenyu.e2e.template.ResourceDataTemplate.newCondition;
+import static org.apache.shenyu.e2e.template.ResourceDataTemplate.newConditions;
+import static org.apache.shenyu.e2e.template.ResourceDataTemplate.newDivideRuleHandle;
+import static org.apache.shenyu.e2e.template.ResourceDataTemplate.newRuleBuilder;
+import static org.apache.shenyu.e2e.template.ResourceDataTemplate.newSelectorBuilder;
+import static org.apache.shenyu.e2e.template.ResourceDataTemplate.newUpstreamsBuilder;
+import static org.hamcrest.text.IsEmptyString.isEmptyOrNullString;
+
+public class DividePluginCases implements ShenYuScenarioProvider {
+ private static final String ANYTHING = "/anything";
+
+ @Override
+ public List get() {
+ return Lists.newArrayList(
+ testDivideWithUriEquals(),
+ testDivideWithUriPathPattern(),
+ testDivideWithUriStartWith(),
+ testDivideWithEndWith(),
+ testDivideWithMethodGet(),
+ testDivideWithMethodPost(),
+ testDivideWithMethodPut(),
+ testDivideWithMethodDelete()
+ );
+ }
+
+ private ShenYuScenarioSpec testDivideWithUriEquals() {
+ return ShenYuScenarioSpec.builder()
+ .name("single-divide uri =]")
+ .beforeEachSpec(
+ ShenYuBeforeEachSpec.builder()
+ .addSelectorAndRule(
+ newSelectorBuilder("httpbin", Plugin.DIVIDE)
+ .handle(newUpstreamsBuilder("httpbin.org"))
+ .conditionList(newConditions(ParamType.URI, Operator.EQUAL, ANYTHING))
+ .build(),
+ newBindingData("httpbin", Plugin.DIVIDE.getAlias(), "httpbin.org"),
+ newRuleBuilder("rule")
+ .handle(newDivideRuleHandle())
+ .conditionList(newConditions(ParamType.URI, Operator.EQUAL, ANYTHING))
+ .build()
+ )
+ .checker(notExists(ANYTHING))
+ .waiting(exists(ANYTHING))
+ .build()
+ )
+ .caseSpec(
+ ShenYuCaseSpec.builder()
+ .addExists(ANYTHING)
+ .addNotExists("/anythin")
+ .addNotExists(ANYTHING + "/x")
+ .addNotExists("/get")
+ .build()
+ )
+ .afterEachSpec(ShenYuAfterEachSpec.builder().deleteWaiting(notExists(ANYTHING)).build())
+ .build();
+ }
+
+ /**
+ * test divide with uri path pattern.
+ * @return ShenYuScenarioSpec
+ */
+ public ShenYuScenarioSpec testDivideWithUriPathPattern() {
+ return ShenYuScenarioSpec.builder()
+ .name("single-divide uri path_pattern]")
+ .beforeEachSpec(
+ ShenYuBeforeEachSpec.builder()
+ .addSelectorAndRule(
+ newSelectorBuilder("httpbin", Plugin.DIVIDE)
+ .handle(newUpstreamsBuilder("httpbin.org"))
+ .conditionList(newConditions(ParamType.URI, Operator.PATH_PATTERN, "/anything/xx/**"))
+ .build(),
+ newBindingData("httpbin", Plugin.DIVIDE.getAlias(), "httpbin.org"),
+ newRuleBuilder("rule")
+ .handle(newDivideRuleHandle())
+ .conditionList(newConditions(ParamType.URI, Operator.PATH_PATTERN, "/anything/xx/**"))
+ .build()
+ )
+ .checker(notExists(ANYTHING + "/xx/yyy"))
+ .waiting(exists(ANYTHING + "/xx/yyy"))
+ .build()
+ )
+ .caseSpec(
+ ShenYuCaseSpec.builder()
+ .addExists(ANYTHING + "/xx")
+ .addExists(ANYTHING + "/xx/yy")
+ .addNotExists(ANYTHING + "/x")
+ .addNotExists(ANYTHING + "/x")
+ .addExists(Method.POST, ANYTHING + "/xx/yy")
+ .addExists(Method.PUT, ANYTHING + "/xx/yy")
+ .addExists(Method.DELETE, ANYTHING + "/xx/yy")
+ .build()
+ )
+ .afterEachSpec(ShenYuAfterEachSpec.builder().deleteWaiting(notExists(ANYTHING + "/xx/yyy")).build())
+ .build();
+ }
+
+ /**
+ * test divide with uri start with.
+ * @return ShenYuScenarioSpec
+ */
+ public ShenYuScenarioSpec testDivideWithUriStartWith() {
+ return ShenYuScenarioSpec.builder()
+ .name("single-divide uri starts_with]")
+ .beforeEachSpec(
+ ShenYuBeforeEachSpec.builder()
+ .addSelectorAndRule(
+ newSelectorBuilder("httpbin", Plugin.DIVIDE)
+ .handle(newUpstreamsBuilder("httpbin.org"))
+ .conditionList(newConditions(ParamType.URI, Operator.STARTS_WITH, ANYTHING + "/xx"))
+ .build(),
+ newBindingData("httpbin", Plugin.DIVIDE.getAlias(), "httpbin.org"),
+ newRuleBuilder("rule")
+ .handle(newDivideRuleHandle())
+ .conditionList(newConditions(ParamType.URI, Operator.STARTS_WITH, ANYTHING + "/xx"))
+ .build()
+ )
+ .checker(notExists(ANYTHING + "/xx"))
+ .waiting(exists(ANYTHING + "/xx"))
+ .build()
+ )
+ .caseSpec(
+ ShenYuCaseSpec.builder()
+ .addExists(ANYTHING + "/xx/yy")
+ .addExists(ANYTHING + "/xxx")
+ .addNotExists(ANYTHING + "/x")
+ .addExists(Method.POST, ANYTHING + "/xx/yy")
+ .addExists(Method.PUT, ANYTHING + "/xx/yy")
+ .addExists(Method.DELETE, ANYTHING + "/xx/yy")
+ .build()
+ )
+ .afterEachSpec(ShenYuAfterEachSpec.builder().deleteWaiting(notExists(ANYTHING + "/xx")).build())
+ .build();
+ }
+
+ /**
+ * test divide with end with.
+ * @return ShenYuScenarioSpec
+ */
+ public ShenYuScenarioSpec testDivideWithEndWith() {
+ return ShenYuScenarioSpec.builder()
+ .name("single-divide uri ends_with]")
+ .beforeEachSpec(
+ ShenYuBeforeEachSpec.builder()
+ .addSelectorAndRule(
+ newSelectorBuilder("httpbin", Plugin.DIVIDE)
+ .handle(newUpstreamsBuilder("httpbin.org"))
+ .conditionList(newConditions(ParamType.URI, Operator.ENDS_WITH, "/200"))
+ .build(),
+ newBindingData("httpbin", Plugin.DIVIDE.getAlias(), "httpbin.org"),
+ newRuleBuilder("rule")
+ .handle(newDivideRuleHandle())
+ .conditionList(newConditions(ParamType.URI, Operator.ENDS_WITH, "/200"))
+ .build()
+ )
+ .checker(notExists(ANYTHING + "/200"))
+ .waiting(exists(ANYTHING + "/200"))
+ .build()
+ )
+ .caseSpec(
+ ShenYuCaseSpec.builder()
+ .addVerifier("/status/200", isEmptyOrNullString())
+ .addExists("/anything/200")
+ .addNotExists(ANYTHING)
+ .addNotExists("/status/300")
+ .addNotExists("/anything/300")
+ .addVerifier(Method.PUT, "/status/200", isEmptyOrNullString())
+ .addVerifier(Method.POST, "/status/200", isEmptyOrNullString())
+ .addVerifier(Method.DELETE, "/status/200", isEmptyOrNullString())
+ .build()
+ )
+ .afterEachSpec(ShenYuAfterEachSpec.builder().deleteWaiting(notExists(ANYTHING + "/200")).build())
+ .build();
+ }
+
+ /**
+ * test divide with method get.
+ * @return ShenYuScenarioSpec
+ */
+ public ShenYuScenarioSpec testDivideWithMethodGet() {
+ return ShenYuScenarioSpec.builder()
+ .name("single-divide method GET")
+ .beforeEachSpec(
+ ShenYuBeforeEachSpec.builder()
+ .addSelectorAndRule(
+ newSelectorBuilder("httpbin", Plugin.DIVIDE)
+ .handle(newUpstreamsBuilder("httpbin.org"))
+ .conditionList(Lists.newArrayList(
+ newCondition(ParamType.METHOD, Operator.EQUAL, "GET"),
+ newCondition(ParamType.URI, Operator.EQUAL, ANYTHING)
+ ))
+ .build(),
+ newBindingData("httpbin", Plugin.DIVIDE.getAlias(), "httpbin.org"),
+ newRuleBuilder("rule")
+ .handle(newDivideRuleHandle())
+ .conditionList(Lists.newArrayList(
+ newCondition(ParamType.METHOD, Operator.EQUAL, "GET"),
+ newCondition(ParamType.URI, Operator.EQUAL, ANYTHING)
+ ))
+ .build()
+ )
+ .checker(notExists(Method.GET, ANYTHING))
+ .waiting(exists(Method.GET, ANYTHING))
+ .build()
+ )
+ .caseSpec(
+ ShenYuCaseSpec.builder()
+ .addExists(Method.GET, ANYTHING)
+ .addNotExists(Method.POST, ANYTHING)
+ .addNotExists(Method.PUT, ANYTHING)
+ .addNotExists(Method.DELETE, ANYTHING)
+ .addNotExists(Method.GET, "/anythin")
+ .addNotExists(Method.GET, ANYTHING + "/x")
+ .addNotExists(Method.GET, "/get")
+ .build()
+ )
+ .afterEachSpec(ShenYuAfterEachSpec.builder().deleteWaiting(notExists(Method.GET, ANYTHING)).build())
+ .build();
+ }
+
+ /**
+ * test divide with method post.
+ * @return ShenYuScenarioSpec
+ */
+ public ShenYuScenarioSpec testDivideWithMethodPost() {
+ return ShenYuScenarioSpec.builder()
+ .name("single-divide method POST")
+ .beforeEachSpec(
+ ShenYuBeforeEachSpec.builder()
+ .addSelectorAndRule(
+ newSelectorBuilder("httpbin", Plugin.DIVIDE)
+ .handle(newUpstreamsBuilder("httpbin.org"))
+ .conditionList(Lists.newArrayList(
+ newCondition(ParamType.METHOD, Operator.EQUAL, "POST"),
+ newCondition(ParamType.URI, Operator.EQUAL, ANYTHING)
+ ))
+ .build(),
+ newBindingData("httpbin", Plugin.DIVIDE.getAlias(), "httpbin.org"),
+ newRuleBuilder("rule")
+ .handle(newDivideRuleHandle())
+ .conditionList(Lists.newArrayList(
+ newCondition(ParamType.METHOD, Operator.EQUAL, "POST"),
+ newCondition(ParamType.URI, Operator.EQUAL, ANYTHING)
+ ))
+ .build()
+ )
+ .checker(notExists(Method.POST, ANYTHING))
+ .waiting(exists(Method.POST, ANYTHING))
+ .build()
+ )
+ .caseSpec(
+ ShenYuCaseSpec.builder()
+ .addExists(Method.POST, ANYTHING)
+ .addNotExists(Method.GET, ANYTHING)
+ .addNotExists(Method.PUT, ANYTHING)
+ .addNotExists(Method.DELETE, ANYTHING)
+ .addNotExists(Method.POST, "/anythin")
+ .addNotExists(Method.POST, ANYTHING + "/x")
+ .addNotExists(Method.POST, "/get")
+ .build()
+ )
+ .afterEachSpec(ShenYuAfterEachSpec.builder().deleteWaiting(notExists(Method.POST, ANYTHING)).build())
+ .build();
+ }
+
+ /**
+ * test divide with method put.
+ * @return ShenYuScenarioSpec
+ */
+ public ShenYuScenarioSpec testDivideWithMethodPut() {
+ return ShenYuScenarioSpec.builder()
+ .name("single-divide method PUT")
+ .beforeEachSpec(
+ ShenYuBeforeEachSpec.builder()
+ .addSelectorAndRule(
+ newSelectorBuilder("httpbin", Plugin.DIVIDE)
+ .handle(newUpstreamsBuilder("httpbin.org"))
+ .conditionList(Lists.newArrayList(
+ newCondition(ParamType.METHOD, Operator.EQUAL, "PUT"),
+ newCondition(ParamType.URI, Operator.EQUAL, ANYTHING)
+ ))
+ .build(),
+ newBindingData("httpbin", Plugin.DIVIDE.getAlias(), "httpbin.org"),
+ newRuleBuilder("rule")
+ .handle(newDivideRuleHandle())
+ .conditionList(Lists.newArrayList(
+ newCondition(ParamType.METHOD, Operator.EQUAL, "PUT"),
+ newCondition(ParamType.URI, Operator.EQUAL, ANYTHING)
+ ))
+ .build()
+ )
+ .checker(notExists(Method.PUT, ANYTHING))
+ .waiting(exists(Method.PUT, ANYTHING))
+ .build()
+ )
+ .caseSpec(
+ ShenYuCaseSpec.builder()
+ .addExists(Method.PUT, ANYTHING)
+ .addNotExists(Method.GET, ANYTHING)
+ .addNotExists(Method.POST, ANYTHING)
+ .addNotExists(Method.DELETE, ANYTHING)
+ .addNotExists(Method.PUT, "/anythin")
+ .addNotExists(Method.PUT, ANYTHING + "/x")
+ .addNotExists(Method.PUT, "/get")
+ .build()
+ )
+ .afterEachSpec(ShenYuAfterEachSpec.builder().deleteWaiting(notExists(Method.PUT, ANYTHING)).build())
+ .build();
+ }
+
+ /**
+ * test divide with method delete.
+ * @return ShenYuScenarioSpec
+ */
+ public ShenYuScenarioSpec testDivideWithMethodDelete() {
+ return ShenYuScenarioSpec.builder()
+ .name("single-divide method DELETE")
+ .beforeEachSpec(
+ ShenYuBeforeEachSpec.builder()
+ .addSelectorAndRule(
+ newSelectorBuilder("httpbin", Plugin.DIVIDE)
+ .handle(newUpstreamsBuilder("httpbin.org"))
+ .conditionList(Lists.newArrayList(
+ newCondition(ParamType.METHOD, Operator.EQUAL, "DELETE"),
+ newCondition(ParamType.URI, Operator.EQUAL, ANYTHING)
+ ))
+ .build(),
+ newBindingData("httpbin", Plugin.DIVIDE.getAlias(), "httpbin.org"),
+ newRuleBuilder("rule")
+ .handle(newDivideRuleHandle())
+ .conditionList(Lists.newArrayList(
+ newCondition(ParamType.METHOD, Operator.EQUAL, "DELETE"),
+ newCondition(ParamType.URI, Operator.EQUAL, ANYTHING)
+ ))
+ .build()
+ )
+ .checker(notExists(Method.DELETE, ANYTHING))
+ .waiting(exists(Method.DELETE, ANYTHING))
+ .build()
+ )
+ .caseSpec(
+ ShenYuCaseSpec.builder()
+ .addExists(Method.DELETE, ANYTHING)
+ .addNotExists(Method.GET, ANYTHING)
+ .addNotExists(Method.PUT, ANYTHING)
+ .addNotExists(Method.POST, ANYTHING)
+ .addNotExists(Method.DELETE, "/anythin")
+ .addNotExists(Method.DELETE, ANYTHING + "/x")
+ .addNotExists(Method.DELETE, "/get")
+ .build()
+ )
+ .afterEachSpec(ShenYuAfterEachSpec.builder().deleteWaiting(notExists(Method.DELETE, ANYTHING)).build())
+ .build();
+ }
+}
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/src/test/java/org/apache/shenyue/e2e/testcase/cluster/DividePluginTest.java b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/src/test/java/org/apache/shenyue/e2e/testcase/cluster/DividePluginTest.java
new file mode 100644
index 000000000000..d40b23815dd9
--- /dev/null
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-cluster/src/test/java/org/apache/shenyue/e2e/testcase/cluster/DividePluginTest.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyue.e2e.testcase.cluster;
+
+import com.google.common.collect.Lists;
+import org.apache.shenyu.e2e.client.admin.AdminClient;
+import org.apache.shenyu.e2e.client.gateway.GatewayClient;
+import org.apache.shenyu.e2e.engine.annotation.ShenYuScenario;
+import org.apache.shenyu.e2e.engine.annotation.ShenYuTest;
+import org.apache.shenyu.e2e.engine.scenario.specification.AfterEachSpec;
+import org.apache.shenyu.e2e.engine.scenario.specification.BeforeEachSpec;
+import org.apache.shenyu.e2e.engine.scenario.specification.CaseSpec;
+import org.apache.shenyu.e2e.enums.ServiceTypeEnum;
+import org.apache.shenyu.e2e.model.ResourcesData;
+import org.apache.shenyu.e2e.model.ResourcesData.Resource;
+import org.apache.shenyu.e2e.model.data.BindingData;
+import org.apache.shenyu.e2e.model.response.SelectorDTO;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+
+import java.util.List;
+import java.util.Objects;
+
+@ShenYuTest(environments = {
+ @ShenYuTest.Environment(
+ serviceName = "shenyu-e2e-admin",
+ service = @ShenYuTest.ServiceConfigure(moduleName = "shenyu-e2e",
+ baseUrl = "http://localhost:31095",
+ type = ServiceTypeEnum.SHENYU_ADMIN,
+ parameters = {
+ @ShenYuTest.Parameter(key = "username", value = "admin"),
+ @ShenYuTest.Parameter(key = "password", value = "123456")
+ }
+ )
+ ),
+ @ShenYuTest.Environment(
+ serviceName = "shenyu-e2e-gateway",
+ service = @ShenYuTest.ServiceConfigure(moduleName = "shenyu-e2e",
+ baseUrl = "http://localhost:31195",
+ type = ServiceTypeEnum.SHENYU_GATEWAY
+ )
+ )
+})
+
+public class DividePluginTest {
+ private List selectorIds = Lists.newArrayList();
+
+ @BeforeAll
+ void setup(final AdminClient client) {
+ client.login();
+ client.deleteAllSelectors();
+ }
+
+ @BeforeEach
+ void before(final AdminClient client, final GatewayClient gateway, final BeforeEachSpec spec) {
+ spec.getChecker().check(gateway);
+
+ ResourcesData resources = spec.getResources();
+ for (Resource res : resources.getResources()) {
+ SelectorDTO dto = client.create(res.getSelector());
+ selectorIds.add(dto.getId());
+ res.getRules().forEach(rule -> {
+ rule.setSelectorId(dto.getId());
+ client.create(rule);
+ });
+ BindingData bindingData = res.getBindingData();
+ if (Objects.nonNull(bindingData)) {
+ bindingData.setSelectorId(dto.getId());
+ client.bindingData(bindingData);
+ }
+ }
+
+ spec.getWaiting().waitFor(gateway);
+ }
+
+ @AfterEach
+ void after(final AdminClient client, final GatewayClient gateway, final AfterEachSpec spec) {
+ spec.getDeleter().delete(client, selectorIds);
+ spec.deleteWaiting().waitFor(gateway);
+ selectorIds = Lists.newArrayList();
+ }
+
+ @ShenYuScenario(provider = DividePluginCases.class)
+ void testDivide(final GatewayClient gateway, final CaseSpec spec) {
+ spec.getVerifiers().forEach(verifier -> verifier.verify(gateway.getHttpRequesterSupplier().get()));
+ }
+
+ @AfterAll
+ static void teardown(final AdminClient client) {
+ client.deleteAllSelectors();
+ }
+
+}
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-grpc/k8s/script/e2e-grpc-sync.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-grpc/k8s/script/e2e-grpc-sync.sh
index 98be3c133e91..01b94751b99d 100644
--- a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-grpc/k8s/script/e2e-grpc-sync.sh
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-grpc/k8s/script/e2e-grpc-sync.sh
@@ -57,6 +57,7 @@ for sync in ${SYNC_ARRAY[@]}; do
# shellcheck disable=SC2181
if (($?)); then
echo "${sync}-sync-e2e-test failed"
+ kubectl logs "$(kubectl get pod -o wide | grep shenyu-examples-grpc | awk '{print $1}')"
echo "shenyu-admin log:"
echo "------------------"
kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin | awk '{print $1}')"
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-spring-cloud/k8s/script/e2e-springcloud-sync.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-spring-cloud/k8s/script/e2e-springcloud-sync.sh
index c8d1a7b37d6a..365a25998a6c 100644
--- a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-spring-cloud/k8s/script/e2e-springcloud-sync.sh
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-spring-cloud/k8s/script/e2e-springcloud-sync.sh
@@ -59,6 +59,9 @@ for sync in ${SYNC_ARRAY[@]}; do
# shellcheck disable=SC2181
if (($?)); then
echo "${sync}-sync-e2e-test failed"
+ echo "shenyu-examples-springcloud log:"
+ echo "------------------"
+ kubectl logs "$(kubectl get pod -o wide | grep shenyu-examples-springcloud | awk '{print $1}')"
echo "shenyu-admin log:"
echo "------------------"
kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin | awk '{print $1}')"
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-h2.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-h2.sh
index c5f39a9f049c..aa9de579c9d5 100644
--- a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-h2.sh
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-h2.sh
@@ -31,8 +31,13 @@ kubectl get pod -o wide
chmod +x "${curPath}"/healthcheck.sh
sh "${curPath}"/healthcheck.sh h2 http://localhost:31095/actuator/health http://localhost:31195/actuator/health
-## run e2e-test
+kubectl get pod -o wide
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin | awk '{print $1}')"
+
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-bootstrap | awk '{print $1}')"
+## run e2e-test
+sleep 60s
curl -S "http://localhost:31195/actuator/pluginData"
./mvnw -B -f ./shenyu-e2e/pom.xml -pl shenyu-e2e-case/shenyu-e2e-case-storage -am test
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-mysql.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-mysql.sh
index 4a5188e0e7a5..6d38e11ca96c 100644
--- a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-mysql.sh
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-mysql.sh
@@ -34,10 +34,14 @@ sleep 30s
chmod +x "${curPath}"/healthcheck.sh
sh "${curPath}"/healthcheck.sh mysql http://localhost:31095/actuator/health http://localhost:31195/actuator/health
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-mysql | awk '{print $1}')"
+
kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin | awk '{print $1}')"
-## run e2e-test
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-bootstrap | awk '{print $1}')"
+## run e2e-test
+sleep 60s
curl -S "http://localhost:31195/actuator/pluginData"
./mvnw -B -f ./shenyu-e2e/pom.xml -pl shenyu-e2e-case/shenyu-e2e-case-storage -am test
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-opengauss.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-opengauss.sh
index 0cbad86d11a2..9496665e45dc 100644
--- a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-opengauss.sh
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-opengauss.sh
@@ -36,8 +36,13 @@ kubectl get pod -o wide
chmod +x "${curPath}"/healthcheck.sh
sh "${curPath}"/healthcheck.sh postgres http://localhost:31095/actuator/health http://localhost:31195/actuator/health
-## run e2e-test
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-opengauss | awk '{print $1}')"
+
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin | awk '{print $1}')"
+kubectl logs "$(kubectl get pod -o wide | grep shenyu-bootstrap | awk '{print $1}')"
+## run e2e-test
+sleep 60s
curl -S "http://localhost:31195/actuator/pluginData"
./mvnw -B -f ./shenyu-e2e/pom.xml -pl shenyu-e2e-case/shenyu-e2e-case-storage -am test
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-postgres.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-postgres.sh
index d3f396d4902a..9ef7919129db 100644
--- a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-postgres.sh
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-storage/k8s/script/e2e-postgres.sh
@@ -42,7 +42,7 @@ kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin | awk '{print $1}')"
kubectl logs "$(kubectl get pod -o wide | grep shenyu-bootstrap | awk '{print $1}')"
## run e2e-test
-
+sleep 60s
curl -S "http://localhost:31195/actuator/pluginData"
./mvnw -B -f ./shenyu-e2e/pom.xml -pl shenyu-e2e-case/shenyu-e2e-case-storage -am test
diff --git a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/script/e2e-websocket-sync.sh b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/script/e2e-websocket-sync.sh
index e6277d1b06fd..ab015d4a212a 100644
--- a/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/script/e2e-websocket-sync.sh
+++ b/shenyu-e2e/shenyu-e2e-case/shenyu-e2e-case-websocket/k8s/script/e2e-websocket-sync.sh
@@ -57,6 +57,7 @@ for sync in ${SYNC_ARRAY[@]}; do
# shellcheck disable=SC2181
if (($?)); then
echo "${sync}-sync-e2e-test failed"
+ kubectl logs "$(kubectl get pod -o wide | grep shenyu-examples-websocket | awk '{print $1}')"
echo "shenyu-admin log:"
echo "------------------"
kubectl logs "$(kubectl get pod -o wide | grep shenyu-admin | awk '{print $1}')"
diff --git a/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/ShenYuExtension.java b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/ShenYuExtension.java
index ba96c3c17ea7..0d6055ee055a 100644
--- a/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/ShenYuExtension.java
+++ b/shenyu-e2e/shenyu-e2e-engine/src/main/java/org/apache/shenyu/e2e/engine/ShenYuExtension.java
@@ -77,7 +77,7 @@ public ConditionEvaluationResult evaluateExecutionCondition(final ExtensionConte
});
// FIXME check service is available
for (ShenYuTest.Environment environment : environments) {
- if (!SocketUtils.checkUrl(environment.service().baseUrl(), 3000)) {
+ if (!SocketUtils.checkUrl(environment.service().baseUrl(), 10000)) {
throw new AssertionFailedError(environment.serviceName() + ":" + environment.service().baseUrl() + " is not available");
//return ConditionEvaluationResult.disabled(environment.serviceName() + ":" + environment.service().baseUrl() + " is not available");
}
diff --git a/shenyu-examples/shenyu-examples-websocket/shenyu-example-spring-native-websocket/src/main/resources/application.yml b/shenyu-examples/shenyu-examples-websocket/shenyu-example-spring-native-websocket/src/main/resources/application.yml
index d3cf0d8c9926..790365bfe3a3 100644
--- a/shenyu-examples/shenyu-examples-websocket/shenyu-example-spring-native-websocket/src/main/resources/application.yml
+++ b/shenyu-examples/shenyu-examples-websocket/shenyu-example-spring-native-websocket/src/main/resources/application.yml
@@ -59,9 +59,9 @@ shenyu:
logging:
level:
- root: info
- org.springframework.boot: info
- org.apache.ibatis: info
- org.apache.shenyu.bonuspoint: info
+ root: debug
+ org.springframework.boot: debug
+ org.apache.ibatis: debug
+ org.apache.shenyu.bonuspoint: debug
org.apache.shenyu.lottery: debug
org.apache.shenyu: debug
diff --git a/shenyu-examples/shenyu-examples-websocket/shenyu-example-spring-reactive-websocket/src/main/resources/application.yml b/shenyu-examples/shenyu-examples-websocket/shenyu-example-spring-reactive-websocket/src/main/resources/application.yml
index 4af1bc143267..57780eae2644 100644
--- a/shenyu-examples/shenyu-examples-websocket/shenyu-example-spring-reactive-websocket/src/main/resources/application.yml
+++ b/shenyu-examples/shenyu-examples-websocket/shenyu-example-spring-reactive-websocket/src/main/resources/application.yml
@@ -52,10 +52,10 @@ shenyu:
logging:
level:
- root: info
- org.springframework.boot: info
- org.apache.ibatis: info
- org.apache.shenyu.bonuspoint: info
+ root: debug
+ org.springframework.boot: debug
+ org.apache.ibatis: debug
+ org.apache.shenyu.bonuspoint: debug
org.apache.shenyu.lottery: debug
org.apache.shenyu: debug
diff --git a/shenyu-integrated-test/shenyu-integrated-test-k8s-ingress-apache-dubbo/pom.xml b/shenyu-integrated-test/shenyu-integrated-test-k8s-ingress-apache-dubbo/pom.xml
index 00b9df61c4ca..c5ca215b5285 100644
--- a/shenyu-integrated-test/shenyu-integrated-test-k8s-ingress-apache-dubbo/pom.xml
+++ b/shenyu-integrated-test/shenyu-integrated-test-k8s-ingress-apache-dubbo/pom.xml
@@ -103,12 +103,6 @@
${project.version}
-
- org.apache.shenyu
- shenyu-spring-boot-starter-plugin-apache-dubbo
- ${project.version}
-
-
org.apache.shenyu
diff --git a/shenyu-integrated-test/shenyu-integrated-test-k8s-ingress-spring-cloud/pom.xml b/shenyu-integrated-test/shenyu-integrated-test-k8s-ingress-spring-cloud/pom.xml
index cf05f938353f..a7b61edf0dd3 100644
--- a/shenyu-integrated-test/shenyu-integrated-test-k8s-ingress-spring-cloud/pom.xml
+++ b/shenyu-integrated-test/shenyu-integrated-test-k8s-ingress-spring-cloud/pom.xml
@@ -30,18 +30,8 @@
-
- org.apache.shenyu
- shenyu-integrated-test-common
- ${project.version}
-
-
- org.apache.shenyu
- shenyu-spring-boot-starter-plugin-springcloud
- ${project.version}
-
org.springframework.cloud
diff --git a/shenyu-integrated-test/shenyu-integrated-test-upload-plugin/shenyu-integrated-test-upload-plugin-case/src/main/java/org/apache/shenyu/integrated/test/upload/plugin/ShenyuUploadPluginIntegratedApplication.java b/shenyu-integrated-test/shenyu-integrated-test-upload-plugin/shenyu-integrated-test-upload-plugin-case/src/main/java/org/apache/shenyu/integrated/test/upload/plugin/ShenyuUploadPluginIntegratedApplication.java
index 23b2cea84ab3..4b4226598ee6 100644
--- a/shenyu-integrated-test/shenyu-integrated-test-upload-plugin/shenyu-integrated-test-upload-plugin-case/src/main/java/org/apache/shenyu/integrated/test/upload/plugin/ShenyuUploadPluginIntegratedApplication.java
+++ b/shenyu-integrated-test/shenyu-integrated-test-upload-plugin/shenyu-integrated-test-upload-plugin-case/src/main/java/org/apache/shenyu/integrated/test/upload/plugin/ShenyuUploadPluginIntegratedApplication.java
@@ -20,6 +20,9 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+/**
+ * The type Spring cloud integrated bootstrap.
+ */
@SpringBootApplication
public class ShenyuUploadPluginIntegratedApplication {
diff --git a/shenyu-integrated-test/shenyu-integrated-test-websocket/src/main/java/org/apache/shenyu/integrated/test/websocket/WebsocketIntegratedBootstrap.java b/shenyu-integrated-test/shenyu-integrated-test-websocket/src/main/java/org/apache/shenyu/integrated/test/websocket/WebsocketIntegratedBootstrap.java
index c6bcd998dbb1..cbef417632e8 100644
--- a/shenyu-integrated-test/shenyu-integrated-test-websocket/src/main/java/org/apache/shenyu/integrated/test/websocket/WebsocketIntegratedBootstrap.java
+++ b/shenyu-integrated-test/shenyu-integrated-test-websocket/src/main/java/org/apache/shenyu/integrated/test/websocket/WebsocketIntegratedBootstrap.java
@@ -20,6 +20,9 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+/**
+ * The type Spring cloud integrated bootstrap.
+ */
@SpringBootApplication
public class WebsocketIntegratedBootstrap {
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sync-data-center/shenyu-spring-boot-starter-sync-data-websocket/src/test/java/org/apache/shenyu/springboot/starter/sync/data/websocket/WebsocketSyncDataConfigurationTest.java b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sync-data-center/shenyu-spring-boot-starter-sync-data-websocket/src/test/java/org/apache/shenyu/springboot/starter/sync/data/websocket/WebsocketSyncDataConfigurationTest.java
index 6cde1fcfd307..78c2ccf34179 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sync-data-center/shenyu-spring-boot-starter-sync-data-websocket/src/test/java/org/apache/shenyu/springboot/starter/sync/data/websocket/WebsocketSyncDataConfigurationTest.java
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sync-data-center/shenyu-spring-boot-starter-sync-data-websocket/src/test/java/org/apache/shenyu/springboot/starter/sync/data/websocket/WebsocketSyncDataConfigurationTest.java
@@ -20,6 +20,7 @@
import org.apache.shenyu.plugin.sync.data.websocket.WebsocketSyncDataService;
import org.apache.shenyu.plugin.sync.data.websocket.config.WebsocketConfig;
import org.apache.shenyu.sync.data.api.PluginDataSubscriber;
+import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
@@ -28,9 +29,9 @@
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.hamcrest.MatcherAssert.assertThat;
/**
* Test case for {@link WebsocketSyncDataConfiguration}.
@@ -53,7 +54,7 @@ public final class WebsocketSyncDataConfigurationTest {
@Autowired
private WebsocketSyncDataService websocketSyncDataService;
-
+
@Test
public void testWebsocketSyncDataService() {
assertNotNull(websocketSyncDataService);
@@ -61,6 +62,6 @@ public void testWebsocketSyncDataService() {
@Test
public void testWebsocketConfig() {
- assertThat(websocketConfig.getUrls(), is("ws://localhost:9095/websocket"));
+ assertThat(websocketConfig.getUrls(), is(Lists.newArrayList("ws://localhost:9095/websocket")));
}
}
diff --git a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketSyncDataService.java b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketSyncDataService.java
index e25f98049654..33783b791886 100644
--- a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketSyncDataService.java
+++ b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketSyncDataService.java
@@ -18,7 +18,14 @@
package org.apache.shenyu.plugin.sync.data.websocket;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
+import org.apache.shenyu.common.enums.RunningModeEnum;
+import org.apache.shenyu.common.timer.AbstractRoundTask;
+import org.apache.shenyu.common.timer.Timer;
+import org.apache.shenyu.common.timer.TimerTask;
+import org.apache.shenyu.common.timer.WheelTimerFactory;
import org.apache.shenyu.plugin.sync.data.websocket.client.ShenyuWebsocketClient;
import org.apache.shenyu.plugin.sync.data.websocket.config.WebsocketConfig;
import org.apache.shenyu.sync.data.api.AuthDataSubscriber;
@@ -31,36 +38,54 @@
import org.slf4j.LoggerFactory;
import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.concurrent.TimeUnit;
/**
* Websocket sync data service.
*/
public class WebsocketSyncDataService implements SyncDataService {
-
+
/**
* logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(WebsocketSyncDataService.class);
-
+
/**
* see https://github.com/apache/tomcat/blob/main/java/org/apache/tomcat/websocket/Constants.java#L99.
*/
private static final String ORIGIN_HEADER_NAME = "Origin";
-
- private final List clients = new ArrayList<>();
-
+
+ private ShenyuWebsocketClient client;
+
+ private final WebsocketConfig websocketConfig;
+
+ private final PluginDataSubscriber pluginDataSubscriber;
+
+ private final List metaDataSubscribers;
+
+ private final List authDataSubscribers;
+
+ private final List proxySelectorDataSubscribers;
+
+ private final List discoveryUpstreamDataSubscribers;
+
+ private final List clients = Lists.newArrayList();
+
+ private final Timer timer;
+
+ private TimerTask timerTask;
+
/**
* Instantiates a new Websocket sync cache.
*
- * @param websocketConfig the websocket config
+ * @param websocketConfig the websocket config
* @param pluginDataSubscriber the plugin data subscriber
- * @param metaDataSubscribers the meta data subscribers
- * @param authDataSubscribers the auth data subscribers
+ * @param metaDataSubscribers the meta data subscribers
+ * @param authDataSubscribers the auth data subscribers
*/
public WebsocketSyncDataService(final WebsocketConfig websocketConfig,
final PluginDataSubscriber pluginDataSubscriber,
@@ -69,27 +94,135 @@ public WebsocketSyncDataService(final WebsocketConfig websocketConfig,
final List proxySelectorDataSubscribers,
final List discoveryUpstreamDataSubscribers
) {
- String[] urls = StringUtils.split(websocketConfig.getUrls(), ",");
+ this.timer = WheelTimerFactory.getSharedTimer();
+ this.websocketConfig = websocketConfig;
+ this.pluginDataSubscriber = pluginDataSubscriber;
+ this.metaDataSubscribers = metaDataSubscribers;
+ this.authDataSubscribers = authDataSubscribers;
+ this.proxySelectorDataSubscribers = proxySelectorDataSubscribers;
+ this.discoveryUpstreamDataSubscribers = discoveryUpstreamDataSubscribers;
+
+ List urls = websocketConfig.getUrls();
for (String url : urls) {
- try {
+ if (StringUtils.isNotEmpty(websocketConfig.getAllowOrigin())) {
+ Map headers = ImmutableMap.of(ORIGIN_HEADER_NAME, websocketConfig.getAllowOrigin());
+ clients.add(new ShenyuWebsocketClient(URI.create(url), headers, Objects.requireNonNull(pluginDataSubscriber), metaDataSubscribers,
+ authDataSubscribers, proxySelectorDataSubscribers, discoveryUpstreamDataSubscribers));
+ } else {
+ clients.add(new ShenyuWebsocketClient(URI.create(url), Objects.requireNonNull(pluginDataSubscriber),
+ metaDataSubscribers, authDataSubscribers, proxySelectorDataSubscribers, discoveryUpstreamDataSubscribers));
+ }
+ }
+
+ this.timer.add(timerTask = new AbstractRoundTask(null, TimeUnit.SECONDS.toMillis(30)) {
+ @Override
+ public void doRun(final String key, final TimerTask timerTask) {
+ masterCheck();
+ }
+ });
+ }
+
+ private void masterCheck() {
+ LOG.info("master checking task start...");
+ if (CollectionUtils.isEmpty(clients)) {
+ List urls = websocketConfig.getUrls();
+ for (String url : urls) {
if (StringUtils.isNotEmpty(websocketConfig.getAllowOrigin())) {
Map headers = ImmutableMap.of(ORIGIN_HEADER_NAME, websocketConfig.getAllowOrigin());
- clients.add(new ShenyuWebsocketClient(new URI(url), headers, Objects.requireNonNull(pluginDataSubscriber), metaDataSubscribers,
+ clients.add(new ShenyuWebsocketClient(URI.create(url), headers, Objects.requireNonNull(pluginDataSubscriber), metaDataSubscribers,
authDataSubscribers, proxySelectorDataSubscribers, discoveryUpstreamDataSubscribers));
} else {
- clients.add(new ShenyuWebsocketClient(new URI(url), Objects.requireNonNull(pluginDataSubscriber),
+ clients.add(new ShenyuWebsocketClient(URI.create(url), Objects.requireNonNull(pluginDataSubscriber),
metaDataSubscribers, authDataSubscribers, proxySelectorDataSubscribers, discoveryUpstreamDataSubscribers));
}
- } catch (URISyntaxException e) {
- LOG.error("websocket url({}) is error", url, e);
+ }
+ }
+// String masterUrl = "";
+ Iterator iterator = clients.iterator();
+ while (iterator.hasNext()) {
+ ShenyuWebsocketClient websocketClient = iterator.next();
+ if (!websocketClient.isOpen()) {
+ iterator.remove();
+ continue;
+ }
+ String runningMode = websocketClient.getRunningMode();
+ // check running mode
+ if (Objects.equals(runningMode, RunningModeEnum.STANDALONE.name())) {
+ LOG.info("admin running in standalone mode...");
+ timerTask.cancel();
+ return;
+ }
+
+ if (!websocketClient.isConnectedToMaster()) {
+ websocketClient.nowClose();
+ iterator.remove();
}
}
}
-
+
@Override
public void close() {
- for (ShenyuWebsocketClient client : clients) {
+ if (Objects.nonNull(client)) {
client.nowClose();
}
+ if (Objects.nonNull(timerTask)) {
+ timerTask.cancel();
+ }
+ timer.shutdown();
+ }
+
+ /**
+ * get websocket config.
+ *
+ * @return websocket config
+ */
+ public WebsocketConfig getWebsocketConfig() {
+ return websocketConfig;
+ }
+
+ /**
+ * get plugin data subscriber.
+ *
+ * @return plugin data subscriber
+ */
+ public PluginDataSubscriber getPluginDataSubscriber() {
+ return pluginDataSubscriber;
+ }
+
+ /**
+ * get meta data subscriber.
+ *
+ * @return meta data subscriber
+ */
+ public List getMetaDataSubscribers() {
+ return metaDataSubscribers;
+ }
+
+ /**
+ * get auth data subscriber.
+ *
+ * @return auth data subscriber
+ */
+ public List getAuthDataSubscribers() {
+ return authDataSubscribers;
+ }
+
+ /**
+ * get proxy selector data subscriber.
+ *
+ * @return proxy selector data subscriber
+ */
+ public List getProxySelectorDataSubscribers() {
+ return proxySelectorDataSubscribers;
+ }
+
+ /**
+ * get discovery upstream data subscriber.
+ *
+ * @return discovery upstream data subscriber
+ */
+ public List getDiscoveryUpstreamDataSubscribers() {
+ return discoveryUpstreamDataSubscribers;
}
+
}
diff --git a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClient.java b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClient.java
index a6a3f1d3273b..13b04e8ef1e2 100644
--- a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClient.java
+++ b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClient.java
@@ -17,14 +17,17 @@
package org.apache.shenyu.plugin.sync.data.websocket.client;
+import org.apache.shenyu.common.constant.RunningModeConstants;
import org.apache.shenyu.common.dto.WebsocketData;
import org.apache.shenyu.common.enums.ConfigGroupEnum;
import org.apache.shenyu.common.enums.DataEventTypeEnum;
+import org.apache.shenyu.common.enums.RunningModeEnum;
import org.apache.shenyu.common.timer.AbstractRoundTask;
import org.apache.shenyu.common.timer.Timer;
import org.apache.shenyu.common.timer.TimerTask;
import org.apache.shenyu.common.timer.WheelTimerFactory;
import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.common.utils.JsonUtils;
import org.apache.shenyu.plugin.sync.data.websocket.handler.WebsocketDataHandler;
import org.apache.shenyu.sync.data.api.AuthDataSubscriber;
import org.apache.shenyu.sync.data.api.DiscoveryUpstreamDataSubscriber;
@@ -39,34 +42,41 @@
import java.net.URI;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* The type shenyu websocket client.
*/
public final class ShenyuWebsocketClient extends WebSocketClient {
-
+
/**
* logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(ShenyuWebsocketClient.class);
-
+
private volatile boolean alreadySync = Boolean.FALSE;
-
+
private final WebsocketDataHandler websocketDataHandler;
-
+
private final Timer timer;
-
+
private TimerTask timerTask;
-
+
+ private String runningMode;
+
+ private String masterUrl;
+
+ private volatile boolean isConnectedToMaster;
+
/**
* Instantiates a new shenyu websocket client.
*
- * @param serverUri the server uri
- * @param pluginDataSubscriber the plugin data subscriber
- * @param metaDataSubscribers the meta data subscribers
- * @param authDataSubscribers the auth data subscribers
- * @param proxySelectorDataSubscribers proxySelectorDataSubscribers,
+ * @param serverUri the server uri
+ * @param pluginDataSubscriber the plugin data subscriber
+ * @param metaDataSubscribers the meta data subscribers
+ * @param authDataSubscribers the auth data subscribers
+ * @param proxySelectorDataSubscribers proxySelectorDataSubscribers,
* @param discoveryUpstreamDataSubscribers discoveryUpstreamDataSubscribers,
*/
public ShenyuWebsocketClient(final URI serverUri, final PluginDataSubscriber pluginDataSubscriber,
@@ -80,16 +90,16 @@ public ShenyuWebsocketClient(final URI serverUri, final PluginDataSubscriber plu
this.timer = WheelTimerFactory.getSharedTimer();
this.connection();
}
-
+
/**
* Instantiates a new shenyu websocket client.
*
- * @param serverUri the server uri
- * @param headers the headers
- * @param pluginDataSubscriber the plugin data subscriber
- * @param metaDataSubscribers the meta data subscribers
- * @param authDataSubscribers the auth data subscribers
- * @param proxySelectorDataSubscribers proxySelectorDataSubscribers,
+ * @param serverUri the server uri
+ * @param headers the headers
+ * @param pluginDataSubscriber the plugin data subscriber
+ * @param metaDataSubscribers the meta data subscribers
+ * @param authDataSubscribers the auth data subscribers
+ * @param proxySelectorDataSubscribers proxySelectorDataSubscribers,
* @param discoveryUpstreamDataSubscribers discoveryUpstreamDataSubscribers,
*/
public ShenyuWebsocketClient(final URI serverUri, final Map headers,
@@ -103,7 +113,7 @@ public ShenyuWebsocketClient(final URI serverUri, final Map head
this.timer = WheelTimerFactory.getSharedTimer();
this.connection();
}
-
+
private void connection() {
this.connectBlocking();
this.timer.add(timerTask = new AbstractRoundTask(null, TimeUnit.SECONDS.toMillis(10)) {
@@ -129,30 +139,52 @@ public boolean connectBlocking() {
}
return success;
}
-
+
@Override
public void onOpen(final ServerHandshake serverHandshake) {
+ LOG.info("websocket connection server[{}] is opened, sending sync msg", this.getURI().toString());
+ send(DataEventTypeEnum.RUNNING_MODE.name());
if (!alreadySync) {
send(DataEventTypeEnum.MYSELF.name());
alreadySync = true;
}
}
-
+
@Override
public void onMessage(final String result) {
- handleResult(result);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("onMessage server[{}] result({})", this.getURI().toString(), result);
+ }
+
+ Map jsonToMap = JsonUtils.jsonToMap(result);
+ Object eventType = jsonToMap.get(RunningModeConstants.EVENT_TYPE);
+ if (Objects.equals(DataEventTypeEnum.RUNNING_MODE.name(), eventType)) {
+ LOG.info("server[{}] handle running mode result({})", this.getURI().toString(), result);
+ this.runningMode = String.valueOf(jsonToMap.get(RunningModeConstants.RUNNING_MODE));
+ if (Objects.equals(RunningModeEnum.STANDALONE.name(), runningMode)) {
+ return;
+ }
+ this.masterUrl = String.valueOf(jsonToMap.get(RunningModeConstants.MASTER_URL));
+ this.isConnectedToMaster = Boolean.TRUE.equals(jsonToMap.get(RunningModeConstants.IS_MASTER));
+ if (!isConnectedToMaster) {
+ LOG.info("not connected to master, close now, master url:[{}], current url:[{}]", masterUrl, this.getURI().toString());
+ this.nowClose();
+ }
+ } else {
+ handleResult(result);
+ }
}
-
+
@Override
public void onClose(final int i, final String s, final boolean b) {
this.close();
}
-
+
@Override
public void onError(final Exception e) {
LOG.error("websocket server[{}] is error.....", getURI(), e);
}
-
+
@Override
public void close() {
alreadySync = false;
@@ -160,40 +192,71 @@ public void close() {
super.close();
}
}
-
+
/**
* Now close.
* now close. will cancel the task execution.
*/
public void nowClose() {
this.close();
- timerTask.cancel();
+ if (Objects.nonNull(timerTask)) {
+ timerTask.cancel();
+ }
}
-
+
private void healthCheck() {
try {
if (!this.isOpen()) {
this.reconnectBlocking();
} else {
this.sendPing();
+ send(DataEventTypeEnum.RUNNING_MODE.name());
LOG.debug("websocket send to [{}] ping message successful", this.getURI());
}
} catch (Exception e) {
LOG.error("websocket connect is error :{}", e.getMessage());
}
}
-
+
/**
* handle admin message.
*
* @param result result
*/
private void handleResult(final String result) {
- LOG.info("handleResult({})", result);
+ LOG.info("server [{}] handleResult({})", this.getURI().toString(), result);
WebsocketData> websocketData = GsonUtils.getInstance().fromJson(result, WebsocketData.class);
ConfigGroupEnum groupEnum = ConfigGroupEnum.acquireByName(websocketData.getGroupType());
String eventType = websocketData.getEventType();
String json = GsonUtils.getInstance().toJson(websocketData.getData());
websocketDataHandler.executor(groupEnum, json, eventType);
}
+
+ /**
+ * Gets the master url.
+ *
+ * @return the master url
+ */
+ public String getMasterUrl() {
+ return masterUrl;
+ }
+
+ /**
+ * Gets the running mode.
+ *
+ * @return the running mode
+ */
+ public String getRunningMode() {
+ return runningMode;
+ }
+
+ /**
+ * whether connect to master.
+ *
+ * @return whether connect to master
+ */
+ public boolean isConnectedToMaster() {
+ return isConnectedToMaster;
+ }
+
}
diff --git a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/config/WebsocketConfig.java b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/config/WebsocketConfig.java
index eff0f2ed5f31..0c8d458412ee 100644
--- a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/config/WebsocketConfig.java
+++ b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/config/WebsocketConfig.java
@@ -17,6 +17,7 @@
package org.apache.shenyu.plugin.sync.data.websocket.config;
+import java.util.List;
import java.util.Objects;
public class WebsocketConfig {
@@ -25,7 +26,7 @@ public class WebsocketConfig {
* if have more shenyu admin url,please config like this.
* 127.0.0.1:8888,127.0.0.1:8889
*/
- private String urls;
+ private List urls;
/**
* allowOrigin.
@@ -37,7 +38,7 @@ public class WebsocketConfig {
*
* @return urls
*/
- public String getUrls() {
+ public List getUrls() {
return urls;
}
@@ -46,7 +47,7 @@ public String getUrls() {
*
* @param urls urls
*/
- public void setUrls(final String urls) {
+ public void setUrls(final List urls) {
this.urls = urls;
}
diff --git a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClientTest.java b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClientTest.java
index 4b875ba2dfd3..0936a435dd09 100644
--- a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClientTest.java
+++ b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClientTest.java
@@ -88,8 +88,10 @@ public void setUp() {
public void testOnOpen() {
shenyuWebsocketClient = spy(shenyuWebsocketClient);
ServerHandshake serverHandshake = mock(ServerHandshake.class);
+ doNothing().when(shenyuWebsocketClient).send(DataEventTypeEnum.RUNNING_MODE.name());
doNothing().when(shenyuWebsocketClient).send(DataEventTypeEnum.MYSELF.name());
shenyuWebsocketClient.onOpen(serverHandshake);
+ verify(shenyuWebsocketClient).send(DataEventTypeEnum.RUNNING_MODE.name());
verify(shenyuWebsocketClient).send(DataEventTypeEnum.MYSELF.name());
}
diff --git a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/config/WebsocketConfigTest.java b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/config/WebsocketConfigTest.java
index 54d10314d92e..713ccce5561a 100644
--- a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/config/WebsocketConfigTest.java
+++ b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/config/WebsocketConfigTest.java
@@ -17,9 +17,11 @@
package org.apache.shenyu.plugin.sync.data.websocket.config;
+import com.google.common.collect.Lists;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import java.util.List;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -30,7 +32,7 @@
*/
public class WebsocketConfigTest {
- private static final String URLS = "ws://localhost:9095/websocket";
+ private static final List URLS = Lists.newArrayList("ws://localhost:9095/websocket");
private static final String ALLOW_ORIGIN = "ws://localhost:9095";