# 클래스 javadoc 이 적은 성질 /** * Atomic pointer from profile name to its current runtime generation (design §7.2). * *

A swap publishes the replacement first and drains the predecessor afterwards, so a rotation is * never observable as a gap. The single scheduled executor exists only to enforce drain deadlines * and is created lazily; it is shut down with the registry so no thread outlives it. */ # close() 전체 120: public void close() { 121: List all = new ArrayList<>(); 122: runtimes.values().forEach(holder -> all.add(holder.get())); 123: // Retired-but-still-draining generations are closed too; they used to survive registry 124: // shutdown entirely. 125: all.addAll(retired); 126: // Every runtime is closed even when one refuses. forEach stopped at the first exception, so a 127: // single misbehaving pool left every remaining connection, thread and socket open — shutdown 128: // leaked more the worse the failure was. 129: RuntimeException firstFailure = null; 130: for (ClientRuntime runtime : all) { 131: try { 132: runtime.forceClose(); 133: } catch (RuntimeException failure) { 134: if (firstFailure == null) { 135: firstFailure = failure; 136: } else { 137: firstFailure.addSuppressed(failure); 138: } 139: } 140: } 141: retired.clear(); 142: runtimes.clear(); 143: if (firstFailure != null) { 144: throw firstFailure; 145: } 146: ScheduledExecutorService scheduler = drainScheduler.getAndSet(null); 147: if (scheduler != null) { 148: // Await termination: a registry that returns while its drain thread is still alive would 149: // leak a thread per rotation cycle, which the resource-bound suite exists to catch. 150: scheduler.shutdownNow(); 151: try { 152: if (!scheduler.awaitTermination(SHUTDOWN_AWAIT_MILLIS, TimeUnit.MILLISECONDS)) { 153: throw new IllegalStateException("http client drain scheduler did not terminate"); 154: } 155: } catch (InterruptedException interrupted) { 156: Thread.currentThread().interrupt(); 157: } 158: } 159: } # 런타임 닫기는 상태를 먼저 바꾼다 90: /** Forced close at the drain deadline; in-flight calls lose their connections by design. */ 91: public final void forceClose() { 92: close(); 93: } 94: 95: @Override 96: public final void close() { 97: ClientRuntimeState previous = state.getAndSet(ClientRuntimeState.CLOSED); 98: if (previous != ClientRuntimeState.CLOSED) { 99: resourceCloser.run(); 100: } 101: } 102:} # 배수 스레드를 만드는 곳과 그 조건 99: previous.beginDrain(drainTimeout); 100: if (previous.state() != ClientRuntimeState.CLOSED && !drainTimeout.isZero()) { 101: ScheduledFuture unusedDrainDeadline = 102: scheduler() 103: .schedule( 172: return existing; 173: } 174: ScheduledExecutorService created = 175: Executors.newSingleThreadScheduledExecutor( 176: runnable -> { 177: Thread thread = new Thread(runnable, "httpclient-runtime-drain"); 178: thread.setDaemon(true); 179: return thread; # 주석이 감시자로 지목한 묶음 18:/** Repeated rotation must not accumulate generations or threads (design §7.2, §28.8). */ 19:class RuntimeRotationDrainTest { 20: 21: @Test 22: void repeatedRotationDrainsEveryPreviousGenerationAndLeavesNoThread() { 30: try (ClientRuntimeRegistry registry = new ClientRuntimeRegistry(Map.of(first.name(), first))) { 31: for (int generation = 2; generation <= 50; generation++) { 32: ClientRuntime replacement = 33: new ClientRuntime( 34: ClientProfiles.builder("rotating").build(), 35: new RuntimeGeneration(generation), 36: closed::incrementAndGet); 37: registry.swap(first.name(), replacement, Duration.ofSeconds(1)); 38: } 44: PerformanceAssertions.structural("every retired generation was closed", closed.get() == 50); 45: assertThat(first.state()).isEqualTo(ClientRuntimeState.CLOSED); 46: Set threadNames = 47: Thread.getAllStackTraces().keySet().stream() 48: .map(Thread::getName) 49: .collect(Collectors.toSet()); 50: assertThat(threadNames).noneMatch(name -> name.startsWith("httpclient-runtime-drain")); 51: } # 같은 성질을 보는 단위 test @Test void closingTheRegistryReleasesEveryGenerationAndLeavesNoThread() { ClientRuntime first = running(1); ClientRuntime second = running(2); ClientRuntimeRegistry registry = new ClientRuntimeRegistry(Map.of(first.name(), first)); ClientRuntimeLease lease = registry.acquire(first.name()); registry.swap(first.name(), second, Duration.ofSeconds(30)); registry.close(); lease.close(); assertThat(first.state()).isEqualTo(ClientRuntimeState.CLOSED); assertThat(second.state()).isEqualTo(ClientRuntimeState.CLOSED); Set threadNames = Thread.getAllStackTraces().keySet().stream() .map(Thread::getName) .collect(Collectors.toSet()); assertThat(threadNames).noneMatch(name -> name.startsWith("httpclient-runtime-drain")); }