This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 36126024c chore(studio): low-risk production hardening and code
cleanup (#2650)
36126024c is described below
commit 36126024cbf75ae2e0f27cc074b2f39ceb6c8b6e
Author: zhaohai <[email protected]>
AuthorDate: Fri Sep 4 16:41:04 2026 +0800
chore(studio): low-risk production hardening and code cleanup (#2650)
* chore(studio): low-risk production hardening and code cleanup
- Remove empty SecurityConfig placeholder (no Spring Security on classpath)
- Enable in-process caching for settings datasource endpoints
- Tune HikariCP pool size and logback rotation limits
- Add nginx hashed-asset caching and pin Docker base images
- Promote no-explicit-any to error; dedupe format helpers in topic.tsx
Verified: frontend tsc -b clean, backend test-compile clean, targeted
settings/message service+controller tests pass.
* remove md
---
deploy/nginx.conf | 13 +++++++++++
server/Dockerfile | 3 +++
.../apache/rocketmq/studio/StudioApplication.java | 2 ++
.../rocketmq/studio/auth/SecurityConfig.java | 25 ----------------------
.../studio/instance/message/MessageService.java | 5 +++++
.../rocketmq/studio/settings/SettingsService.java | 11 ++++++++++
server/src/main/resources/application.yml | 11 ++++++++++
server/src/main/resources/logback-spring.xml | 5 +++--
web/Dockerfile | 6 ++++--
web/eslint.config.js | 5 ++++-
web/src/pages/instance/topic.tsx | 12 +----------
web/src/pages/settings/GeneralSettingsTab.tsx | 6 ++++--
12 files changed, 61 insertions(+), 43 deletions(-)
diff --git a/deploy/nginx.conf b/deploy/nginx.conf
index 9d9723352..2e23acc93 100644
--- a/deploy/nginx.conf
+++ b/deploy/nginx.conf
@@ -9,6 +9,19 @@ server {
try_files $uri $uri/ /index.html;
}
+ # Hashed asset bundles are content-addressable: long-cache them with
+ # `immutable` so repeat visits skip re-downloading the ~1.78 MB JS payload
+ # (antd vendor + app entry) entirely. The HTML entry stays short-lived so
+ # the next deploy reaches users immediately.
+ location /assets/ {
+ root /usr/share/nginx/html;
+ expires 1y;
+ add_header Cache-Control "public, max-age=31536000, immutable";
+ }
+ location = /index.html {
+ add_header Cache-Control "no-cache";
+ }
+
location /api/ {
proxy_pass http://rocketmq-server:8888;
proxy_set_header Host $host;
diff --git a/server/Dockerfile b/server/Dockerfile
index b4f380efc..2e2b1b1c9 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -1,4 +1,7 @@
# Shared runtime dependencies
+# Note: a pinned dragonwell patch tag (e.g. 21.0.10-anolis) would be
preferable for
+# reproducibility, but such tags are not reachable through the deployment
registry
+# mirrors in use, so keep the rolling :21 tag until a verifiable pin is
available.
FROM alibabadragonwell/dragonwell:21 AS runtime-base
WORKDIR /app
# Node.js + agent CLIs for the claude-code / qoder agent providers.
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/StudioApplication.java
b/server/src/main/java/org/apache/rocketmq/studio/StudioApplication.java
index 38a537af1..1ea2baee9 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/StudioApplication.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/StudioApplication.java
@@ -19,10 +19,12 @@ package org.apache.rocketmq.studio;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
+@EnableCaching
public class StudioApplication {
public static void main(String[] args) {
SpringApplication.run(StudioApplication.class, args);
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/auth/SecurityConfig.java
b/server/src/main/java/org/apache/rocketmq/studio/auth/SecurityConfig.java
deleted file mode 100644
index 927d098d6..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/SecurityConfig.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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.rocketmq.studio.auth;
-
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-public class SecurityConfig {
- // TODO: Add Spring Security configuration when Spring Security dependency
is added
-}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
index 6b391c943..0e29caf91 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
@@ -67,6 +67,11 @@ public class MessageService {
if (page < 1 || pageSize < 1 || pageSize > MAX_PAGE_SIZE) {
throw new BusinessException(400, "page must be positive and
pageSize must be between 1 and 200");
}
+ // The Apache / Aliyun / Tencent providers all cap topic-keyed scans
at a fixed
+ // broker-side limit (see
RocketMQMessageProvider#DEFAULT_TOPIC_LIMIT). RocketMQ has
+ // no server-side "skip the first N messages" API, so the in-memory
subList below is
+ // the page slice within that cap; the total / resultMayBeTruncated
fields let the UI
+ // tell the user when deeper pages may be empty.
MessageQueryResult queryResult = queryMessagesDetailed(
instanceId, topic, msgId, tag, key, startTime, endTime, page
== 1);
List<MessageRecordVO> result = queryResult.messages();
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
index 4b9177c50..7847297d4 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
@@ -29,6 +29,8 @@ import java.net.InetAddress;
import java.net.UnknownHostException;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
@@ -206,11 +208,17 @@ public class SettingsService {
}
+ // The full-list endpoint is hit by every metrics tab on first paint and
+ // every time the datasource dropdown re-fetches. Caching it with the
+ // write paths evicted below keeps the user-visible list correct while
+ // removing a per-tab DB round trip.
+ @Cacheable("data-sources")
public List<DataSourceVO> listDataSources() {
log.debug("Listing all data sources");
return settingsRepository.findAllDataSources();
}
+ @Cacheable(value = "data-sources", key = "'page:' + #search + ':' + #type
+ ':' + #page + ':' + #pageSize")
public PageResult<DataSourceVO> listDataSources(String search, String
type, int page, int pageSize) {
if (page < 1) {
throw new BusinessException(400, "page must be greater than zero");
@@ -224,6 +232,7 @@ public class SettingsService {
}
+ @CacheEvict(value = "data-sources", allEntries = true)
public DataSourceVO createDataSource(DataSourceVO dataSource) {
if (dataSource == null) {
throw new BusinessException(400, "Data source request is
required");
@@ -236,6 +245,7 @@ public class SettingsService {
}
+ @CacheEvict(value = "data-sources", allEntries = true)
public DataSourceVO updateDataSource(DataSourceVO dataSource) {
if (dataSource == null) {
throw new BusinessException(400, "Data source request is
required");
@@ -260,6 +270,7 @@ public class SettingsService {
}
+ @CacheEvict(value = "data-sources", allEntries = true)
public void deleteDataSource(String key) {
String normalizedKey = normalizeDataSourceKey(key);
log.info("Deleting data source: {}", normalizedKey);
diff --git a/server/src/main/resources/application.yml
b/server/src/main/resources/application.yml
index 6efbf2bb5..0b248c8c5 100644
--- a/server/src/main/resources/application.yml
+++ b/server/src/main/resources/application.yml
@@ -11,6 +11,17 @@ spring:
username: ${SPRING_DATASOURCE_USERNAME:root}
password: ${SPRING_DATASOURCE_PASSWORD:studio123}
driver-class-name: com.mysql.cj.jdbc.Driver
+ hikari:
+ # Studio is polled by multiple tabs / dashboards; the Spring Boot
default of 10
+ # connections saturates fast under concurrent filters + alerting. 30
leaves
+ # headroom without overwhelming a default MySQL max-connections=151.
+ maximum-pool-size: 30
+ minimum-idle: 5
+ connection-timeout: 5000
+ idle-timeout: 300000
+ max-lifetime: 1200000
+ pool-name: studio-pool
+ leak-detection-threshold: 30000
mail:
host: ${STUDIO_ALERTING_SMTP_HOST:}
port: ${STUDIO_ALERTING_SMTP_PORT:587}
diff --git a/server/src/main/resources/logback-spring.xml
b/server/src/main/resources/logback-spring.xml
index 6342b99ef..8178b497e 100644
--- a/server/src/main/resources/logback-spring.xml
+++ b/server/src/main/resources/logback-spring.xml
@@ -11,8 +11,9 @@
<file>logs/rocketmq-studio/studio.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/rocketmq-studio/otherdays/studio.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
- <maxFileSize>1GB</maxFileSize>
- <maxHistory>10</maxHistory>
+ <maxFileSize>100MB</maxFileSize>
+ <maxHistory>30</maxHistory>
+ <totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
diff --git a/web/Dockerfile b/web/Dockerfile
index a42e7bd7a..f5beaefce 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -1,5 +1,7 @@
# Stage 1: Build
-FROM node:20-alpine AS build
+# Pin Node and nginx base images to a specific minor so CI builds are
+# reproducible (the bare :20-alpine and :alpine tags are rolling).
+FROM node:20.19.0-alpine AS build
ARG VITE_GIT_COMMIT
ENV VITE_GIT_COMMIT=${VITE_GIT_COMMIT}
WORKDIR /app
@@ -9,7 +11,7 @@ COPY . .
RUN npm run build
# Stage 2: Serve
-FROM nginx:alpine
+FROM nginx:1.27.2-alpine
ENV NGINX_ENVSUBST_FILTER=^RESOLVER$
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/templates/default.conf.template
diff --git a/web/eslint.config.js b/web/eslint.config.js
index cb4f6fceb..46a73af7d 100644
--- a/web/eslint.config.js
+++ b/web/eslint.config.js
@@ -20,7 +20,10 @@ export default tseslint.config(
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport:
true }],
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_'
}],
- '@typescript-eslint/no-explicit-any': 'warn',
+ // The codebase had 0 explicit `any` at the 2026-08 cleanup; keep it
that way
+ // by making the rule an error so any new escape hatch shows up in CI
instead
+ // of slipping through as a warning.
+ '@typescript-eslint/no-explicit-any': 'error',
},
},
);
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index e029b5da7..df927c046 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -86,6 +86,7 @@ import {
type ResourceImportRow,
} from '../../utils/resourceCsvImport';
import { downloadCsv } from '../../utils/download';
+import { formatDateTime, formatNumber } from '../../utils/format';
import { tableScrollX } from '../../utils/table';
import {
analyzeTopicRoutes,
@@ -287,17 +288,6 @@ const RANDOM_BODY_GENERATORS = [
{ label: '监控指标', fn: randomMetricsBody },
];
-// ─── Format helpers ───────────────────────────────────────────────
-const formatNumber = (n: number) => n.toLocaleString('zh-CN');
-
-const formatDateTime = (iso?: string): string => {
- if (!iso) return '-';
- const d = new Date(iso);
- if (Number.isNaN(d.getTime())) return '-';
- const pad = (n: number) => String(n).padStart(2, '0');
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}
${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
-};
-
const ROUTE_STATUS_META: Record<
RouteDiagnosticStatus,
{ color: string; label: string; icon: React.ReactNode }
diff --git a/web/src/pages/settings/GeneralSettingsTab.tsx
b/web/src/pages/settings/GeneralSettingsTab.tsx
index facafb052..e7ce47fc3 100644
--- a/web/src/pages/settings/GeneralSettingsTab.tsx
+++ b/web/src/pages/settings/GeneralSettingsTab.tsx
@@ -187,8 +187,10 @@ export const GeneralSettingsTab = () => {
if (!(await mergeAndSave(values))) return;
await testNotification(channel);
message.success(t('settings.testMessageSent'));
- } catch (error: any) {
- message.error(error?.response?.data?.message ??
t('settings.testMessageFailed'));
+ } catch (error) {
+ const apiMessage = (error as { response?: { data?: { message?: unknown }
} })?.response?.data
+ ?.message;
+ message.error(typeof apiMessage === 'string' ? apiMessage :
t('settings.testMessageFailed'));
} finally {
setTestingChannel(undefined);
}