diff --git a/db_migration_device_priority_prefixes.sql b/db_migration_device_priority_prefixes.sql new file mode 100644 index 0000000..6611a5c --- /dev/null +++ b/db_migration_device_priority_prefixes.sql @@ -0,0 +1,19 @@ +-- 添加设备优先前缀配置项 +-- 该配置用于指定设备选择时的前缀优先级,逗号分隔 +-- 例如:"xx,yy,zz" 表示优先选择 xx 开头的设备,其次 yy,最后 zz +-- 同优先级内随机选择,实现负载均衡 + +INSERT INTO system_config (config_key, config_value, config_type, description, is_system, created_at, updated_at) +VALUES ( + 'device.priority_prefixes', + '', + 'STRING', + '设备选择优先前缀(逗号分隔),例如:xx,yy,zz。优先选择匹配前缀的设备,同优先级内随机。为空则全随机', + 1, + NOW(3), + NOW(3) +) +ON DUPLICATE KEY UPDATE + config_type = 'STRING', + description = '设备选择优先前缀(逗号分隔),例如:xx,yy,zz。优先选择匹配前缀的设备,同优先级内随机。为空则全随机', + updated_at = NOW(3); diff --git a/docs/设备优先前缀配置说明.md b/docs/设备优先前缀配置说明.md new file mode 100644 index 0000000..579b946 --- /dev/null +++ b/docs/设备优先前缀配置说明.md @@ -0,0 +1,162 @@ +# 设备优先前缀配置说明 + +## 功能概述 + +设备优先前缀配置允许管理员指定在空闲设备选择时的优先级规则,优先选择特定前缀的设备,实现更灵活的设备分配策略。 + +## 配置项 + +**配置键**: `device.priority_prefixes` +**配置类型**: STRING +**默认值**: 空字符串(全随机) +**格式**: 逗号分隔的前缀列表 + +## 使用方法 + +### 1. 配置优先前缀 + +通过系统配置接口设置 `device.priority_prefixes` 的值。 + +**示例配置值**: +``` +xx,yy,zz +``` + +这表示: +- 第1优先级:`xx` 开头的设备 +- 第2优先级:`yy` 开头的设备 +- 第3优先级:`zz` 开头的设备 +- 最低优先级:其他所有设备 + +### 2. 选择逻辑 + +设备分配时会按照以下逻辑进行: + +1. **过滤占用设备**:排除被其他任务占用的设备 +2. **按前缀分组**:根据配置的前缀将设备分成不同优先级组 +3. **组内随机**:每个优先级组内的设备随机打乱 +4. **顺序尝试**:按优先级从高到低依次尝试分配,直到成功 + +**特点**: +- ✅ 优先使用指定前缀的设备 +- ✅ 同优先级内随机选择,避免总是选中同一台设备 +- ✅ 实现负载均衡和优先级控制的平衡 +- ✅ 支持冷却机制和并发安全 + +### 3. 示例场景 + +#### 场景1:无配置(默认行为) +``` +device.priority_prefixes = "" +``` +**效果**:所有设备完全随机选择,与原有逻辑一致 + +#### 场景2:优先使用xx设备 +``` +device.priority_prefixes = "xx" +``` +**假设可用设备**:`xx1, xx2, yy1, yy2, zz1` + +**选择顺序**: +1. 优先在 `xx1, xx2` 中随机选择 +2. 如果 xx 设备都在冷却期或被占用,再从 `yy1, yy2, zz1` 中随机选择 + +#### 场景3:多级优先级 +``` +device.priority_prefixes = "xx,yy" +``` +**假设可用设备**:`xx1, xx2, yy1, yy2, zz1, zz2` + +**选择顺序**: +1. 第1优先级:在 `xx1, xx2` 中随机选择 +2. 第2优先级:如果 xx 都不可用,在 `yy1, yy2` 中随机选择 +3. 第3优先级:如果 xx、yy 都不可用,在 `zz1, zz2` 中随机选择 + +## API使用示例 + +### 查询当前配置 +```bash +GET /api/admin/config/key/device.priority_prefixes +``` + +### 更新配置 +```bash +PUT /api/admin/config + +{ + "configKey": "device.priority_prefixes", + "configValue": "xx,yy,zz", + "configType": "STRING", + "description": "设备选择优先前缀(逗号分隔)" +} +``` + +### 批量更新(推荐) +```bash +PUT /api/admin/config/batch + +[ + { + "configKey": "device.priority_prefixes", + "configValue": "xx,yy" + } +] +``` + +## 日志输出 + +启用该功能后,日志中会输出相关信息: + +``` +[INFO] 使用设备优先前缀配置:[xx, yy] +[DEBUG] 优先级0(前缀=xx):2 台设备 +[DEBUG] 优先级1(前缀=yy):3 台设备 +[DEBUG] 无优先级设备:5 台 +[INFO] 设备列表已按优先级排序并随机化:[xx2, xx1, yy3, yy1, yy2, zz1, aa1, bb1, cc1, dd1] +[INFO] 设备分配成功:设备=xx2, 任务ID=123, 原因=首次选区 +``` + +## 注意事项 + +1. **前缀匹配**:使用 `startsWith()` 进行前缀匹配,例如配置 `xx` 会匹配 `xx1`、`xx2`、`xxabc` 等 +2. **大小写敏感**:前缀匹配区分大小写 +3. **空格处理**:配置值会自动去除前后空格,`"xx, yy"` 会被处理为 `["xx", "yy"]` +4. **空配置**:如果配置为空或不存在,系统会退化到原有的完全随机选择逻辑 +5. **优先级顺序**:前缀在配置字符串中的顺序决定优先级,越靠前优先级越高 +6. **实时生效**:配置修改后立即生效,无需重启服务 + +## 数据库迁移 + +执行以下 SQL 脚本添加配置项: + +```sql +-- 位置: db_migration_device_priority_prefixes.sql +INSERT INTO system_config (config_key, config_value, config_type, description, is_system, created_at, updated_at) +VALUES ( + 'device.priority_prefixes', + '', + 'STRING', + '设备选择优先前缀(逗号分隔),例如:xx,yy,zz。优先选择匹配前缀的设备,同优先级内随机。为空则全随机', + 1, + NOW(3), + NOW(3) +) +ON DUPLICATE KEY UPDATE + config_type = 'STRING', + description = '设备选择优先前缀(逗号分隔),例如:xx,yy,zz。优先选择匹配前缀的设备,同优先级内随机。为空则全随机', + updated_at = NOW(3); +``` + +## 相关代码 + +- **配置服务**: `SystemConfigService.getDevicePriorityPrefixes()` +- **分配服务**: `DeviceAllocationService.allocateDevice()` +- **排序逻辑**: `DeviceAllocationService.sortByPrefixPriority()` + +## 测试建议 + +1. **无配置测试**:不配置前缀,验证随机选择是否正常 +2. **单前缀测试**:配置单个前缀,验证是否优先选择该前缀设备 +3. **多前缀测试**:配置多个前缀,验证优先级顺序是否正确 +4. **负载均衡测试**:多次分配,验证同优先级内是否随机分布 +5. **冷却期测试**:验证高优先级设备在冷却期时,能否正确选择低优先级设备 diff --git a/logs/audit-status.2025-10-03.0.log.gz b/logs/audit-status.2025-10-03.0.log.gz new file mode 100644 index 0000000..d929037 Binary files /dev/null and b/logs/audit-status.2025-10-03.0.log.gz differ diff --git a/logs/server.2025-10-03.0.log.gz b/logs/server.2025-10-03.0.log.gz new file mode 100644 index 0000000..844979d Binary files /dev/null and b/logs/server.2025-10-03.0.log.gz differ diff --git a/src/main/java/com/gameplatform/server/device/Detection.java b/src/main/java/com/gameplatform/server/device/Detection.java index 3d8c72c..4ab66b8 100644 --- a/src/main/java/com/gameplatform/server/device/Detection.java +++ b/src/main/java/com/gameplatform/server/device/Detection.java @@ -119,6 +119,10 @@ public class Detection { return this.lastSnapshot; } + // 失败计数器(用于监控) + private volatile int consecutiveFailures = 0; + private static final int MAX_FAILURES_BEFORE_ALERT = 3; + /** * 定时任务:全量拉取并交由 DeviceStats 更新内存分类与审计。 * 默认每 30 秒执行一次,可通过配置覆盖:detection.poll.cron 或 detection.poll.fixedDelayMs @@ -128,7 +132,14 @@ public class Detection { try { log.info("定时拉取设备快照并更新统计"); DeviceStatusResponse snapshot = listAllDevices(); + if (snapshot != null) { + // 重置失败计数器 + if (consecutiveFailures > 0) { + log.info("设备状态获取恢复正常,重置失败计数器(之前连续失败{}次)", consecutiveFailures); + consecutiveFailures = 0; + } + deviceStats.updateWithSnapshot(snapshot); log.info("设备快照更新统计完成"); @@ -155,9 +166,36 @@ public class Detection { } catch (Exception ex) { log.warn("基于设备快照推进USING→LOGGED_IN时出现异常", ex); } + } else { + // 快照获取失败,使用旧缓存继续部分功能 + DeviceStatusResponse cachedSnapshot = getLastSnapshot(); + if (cachedSnapshot != null) { + log.warn("使用缓存的设备快照数据(可能过期),避免服务完全中断"); + // 不更新统计,但可以尝试继续状态推进 + } else { + log.warn("无可用设备快照(包括缓存),跳过本轮更新"); + } } } catch (Exception e) { - log.error("定时拉取设备快照并更新统计失败", e); + consecutiveFailures++; + + // 判断异常类型,给出更明确的错误信息 + String errorType = "未知错误"; + if (e instanceof org.springframework.web.reactive.function.client.WebClientRequestException) { + if (e.getMessage().contains("connection timed out") || e.getMessage().contains("ConnectTimeoutException")) { + errorType = "连接超时"; + } else if (e.getMessage().contains("Connection refused")) { + errorType = "连接被拒绝"; + } + } + + if (consecutiveFailures >= MAX_FAILURES_BEFORE_ALERT) { + log.error("【告警】定时拉取设备快照连续失败{}次(错误类型:{}),请检查脚本服务器连接: {}", + consecutiveFailures, errorType, e.getMessage()); + } else { + log.error("定时拉取设备快照并更新统计失败(连续失败{}次,错误类型:{})", + consecutiveFailures, errorType, e); + } } } diff --git a/src/main/java/com/gameplatform/server/service/admin/SystemConfigService.java b/src/main/java/com/gameplatform/server/service/admin/SystemConfigService.java index 9c83619..aec7115 100644 --- a/src/main/java/com/gameplatform/server/service/admin/SystemConfigService.java +++ b/src/main/java/com/gameplatform/server/service/admin/SystemConfigService.java @@ -128,6 +128,19 @@ public class SystemConfigService { return getConfigValue("user.custom_scan_content", ""); } + // 获取设备优先前缀列表(逗号分隔),返回List + public List getDevicePriorityPrefixes() { + String value = getConfigValue("device.priority_prefixes", ""); + if (value == null || value.trim().isEmpty()) { + return List.of(); + } + // 按逗号分隔,去除空白,过滤空字符串 + return java.util.Arrays.stream(value.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(java.util.stream.Collectors.toList()); + } + // 批量更新配置 public boolean updateConfigs(List configs) { if (configs == null || configs.isEmpty()) { diff --git a/src/main/java/com/gameplatform/server/service/link/DeviceAllocationService.java b/src/main/java/com/gameplatform/server/service/link/DeviceAllocationService.java index b4b99e4..e37adc3 100644 --- a/src/main/java/com/gameplatform/server/service/link/DeviceAllocationService.java +++ b/src/main/java/com/gameplatform/server/service/link/DeviceAllocationService.java @@ -3,6 +3,7 @@ package com.gameplatform.server.service.link; import com.gameplatform.server.mapper.agent.LinkTaskMapper; import com.gameplatform.server.model.entity.agent.LinkTask; import com.gameplatform.server.service.cooldown.MemoryMachineCooldownService; +import com.gameplatform.server.service.admin.SystemConfigService; import com.gameplatform.server.mapper.history.LinkTaskStatusHistoryMapper; import com.gameplatform.server.model.entity.history.LinkTaskStatusHistory; import org.slf4j.Logger; @@ -28,13 +29,16 @@ public class DeviceAllocationService { private final MemoryMachineCooldownService machineCooldownService; private final LinkTaskMapper linkTaskMapper; private final LinkTaskStatusHistoryMapper statusHistoryMapper; + private final SystemConfigService systemConfigService; public DeviceAllocationService(MemoryMachineCooldownService machineCooldownService, LinkTaskMapper linkTaskMapper, - LinkTaskStatusHistoryMapper statusHistoryMapper) { + LinkTaskStatusHistoryMapper statusHistoryMapper, + SystemConfigService systemConfigService) { this.machineCooldownService = machineCooldownService; this.linkTaskMapper = linkTaskMapper; this.statusHistoryMapper = statusHistoryMapper; + this.systemConfigService = systemConfigService; } /** @@ -64,14 +68,13 @@ public class DeviceAllocationService { log.info("设备占用检查完成:原候选设备数={}, 过滤后设备数={}, 可用设备={}", availableDevices.size(), filteredDevices.size(), filteredDevices); - // 2. 打乱设备列表,实现负载均衡 - List shuffledDevices = new ArrayList<>(filteredDevices); - Collections.shuffle(shuffledDevices, ThreadLocalRandom.current()); + // 2. 按前缀优先级排序,然后打乱(同优先级内随机) + List sortedDevices = sortByPrefixPriority(filteredDevices); - log.info("设备列表已随机化:{}", shuffledDevices); + log.info("设备列表已按优先级排序并随机化:{}", sortedDevices); - // 3. 尝试原子分配设备(按随机顺序) - for (String deviceId : shuffledDevices) { + // 3. 尝试原子分配设备(按优先级顺序) + for (String deviceId : sortedDevices) { if (machineCooldownService.tryAllocateDevice(deviceId, reason, linkTaskId)) { log.info("设备分配成功:设备={}, 任务ID={}, 原因={}", deviceId, linkTaskId, reason); return deviceId; @@ -186,6 +189,73 @@ public class DeviceAllocationService { return availableDevices; } + /** + * 根据配置的前缀优先级对设备列表进行排序 + * 优先级高的前缀排在前面,同优先级内随机打乱 + * @param devices 原始设备列表 + * @return 排序后的设备列表 + */ + private List sortByPrefixPriority(List devices) { + if (devices == null || devices.isEmpty()) { + return devices; + } + + // 获取配置的优先前缀列表 + List priorityPrefixes = systemConfigService.getDevicePriorityPrefixes(); + + if (priorityPrefixes.isEmpty()) { + // 没有配置优先前缀,直接随机打乱(保持原有行为) + List shuffled = new ArrayList<>(devices); + Collections.shuffle(shuffled, ThreadLocalRandom.current()); + return shuffled; + } + + log.info("使用设备优先前缀配置:{}", priorityPrefixes); + + // 按优先级分组:优先级索引 -> 设备列表 + // -1 表示没有匹配任何优先前缀 + List> groups = new ArrayList<>(); + for (int i = 0; i <= priorityPrefixes.size(); i++) { + groups.add(new ArrayList<>()); + } + + // 将设备分组 + for (String deviceId : devices) { + int priorityIndex = -1; + // 查找设备匹配的最高优先级前缀 + for (int i = 0; i < priorityPrefixes.size(); i++) { + if (deviceId.startsWith(priorityPrefixes.get(i))) { + priorityIndex = i; + break; // 找到第一个匹配的前缀就停止 + } + } + + if (priorityIndex >= 0) { + groups.get(priorityIndex).add(deviceId); + } else { + // 没有匹配的放到最后一组 + groups.get(groups.size() - 1).add(deviceId); + } + } + + // 对每组内部进行随机打乱,然后按优先级顺序合并 + List result = new ArrayList<>(); + for (int i = 0; i < groups.size(); i++) { + List group = groups.get(i); + if (!group.isEmpty()) { + Collections.shuffle(group, ThreadLocalRandom.current()); + result.addAll(group); + if (i < priorityPrefixes.size()) { + log.debug("优先级{}(前缀={}):{} 台设备", i, priorityPrefixes.get(i), group.size()); + } else { + log.debug("无优先级设备:{} 台", group.size()); + } + } + } + + return result; + } + /** * 验证设备分配结果(分配后的双重检查) * @param deviceId 设备ID