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";