yeg
2026-09-29 b78295e8da96c3e5e76befeef9582ce5273dbb74
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.battery.business.web;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.*;
import jakarta.annotation.PreDestroy;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.battery.business.service.ITestLiveService;
import com.battery.business.service.ITestLiveService.LiveState;
/** 在 HTTP 边界托管 SSE 连接和最新状态推送。 */
@Component
public class TestLiveStream {
    private final ITestLiveService live;
    /**
     * SSE 推送线程池显式创建并命名,便于按连接排查推送任务。
     *
     * <p>连接关闭后立即移除已取消的定点任务,避免取消任务在延迟队列里滞留到下一个周期。</p>
     */
    private final ScheduledThreadPoolExecutor scheduler = scheduler();
    public TestLiveStream(ITestLiveService live) { this.live = live; }
 
    private static ScheduledThreadPoolExecutor scheduler()
    {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2,
                BasicThreadFactory.builder().namingPattern("sse-live-pool-%d").daemon(true).build());
        executor.setRemoveOnCancelPolicy(true);
        return executor;
    }
 
    /** 建立指定测试的 SSE 连接,先推一次当前状态,随后每 3 秒推一次;连接完成、超时或出错时取消该推送任务。 */
    public SseEmitter stream(Long testId)
    {
        SseEmitter emitter = createEmitter();
        try { sendAll(emitter, testId); }
        catch (IOException exception)
        {
            // 客户端刷新或离开页面会断开连接,不能再交给全局异常处理器写 SSE JSON。
            emitter.complete();
            return emitter;
        }
        catch (RuntimeException exception) { emitter.completeWithError(exception); return emitter; }
        ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(() -> {
            try { sendAll(emitter, testId); }
            catch (IOException exception)
            {
                // 写出失败表示 SSE 客户端已断开,静默结束连接。
                emitter.complete();
            }
            catch (RuntimeException exception) { emitter.completeWithError(exception); }
        }, 3, 3, TimeUnit.SECONDS);
        emitter.onCompletion(() -> future.cancel(false));
        emitter.onTimeout(() -> future.cancel(false));
        emitter.onError(error -> future.cancel(false));
        return emitter;
    }
 
    /** 创建实时流发射器,供断连生命周期测试替换。 */
    protected SseEmitter createEmitter()
    {
        return new SseEmitter(0L);
    }
 
    private void sendAll(SseEmitter emitter, Long testId) throws IOException
    {
        LiveState state = live.current(testId);
        emitter.send(SseEmitter.event().name("snapshot").data(state));
        emitter.send(SseEmitter.event().name("device-status").data(state.sources()));
        emitter.send(SseEmitter.event().name("reception-status").data(Map.of("testId", testId, "receptionStatus", state.receptionStatus())));
        emitter.send(SseEmitter.event().name("test-status").data(Map.of("testId", testId, "testStatus", state.testStatus())));
        emitter.send(SseEmitter.event().name("exception").data(state.exceptions()));
        emitter.send(SseEmitter.event().name("heartbeat").data(Map.of("at", state.updatedAt())));
    }
 
 
    @PreDestroy public void close() { scheduler.shutdownNow(); }
}