# 클래스 javadoc 이 적은 성질
/**
 * Atomic pointer from profile name to its current runtime generation (design §7.2).
 *
 * <p>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<ClientRuntime> 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<String> 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<String> threadNames =
        Thread.getAllStackTraces().keySet().stream()
            .map(Thread::getName)
            .collect(Collectors.toSet());
    assertThat(threadNames).noneMatch(name -> name.startsWith("httpclient-runtime-drain"));
  }
