| 102 |
102 |
import com.goldencode.p2j.net.SessionManager;
|
| 103 |
103 |
import com.goldencode.p2j.net.RouterSessionManager;
|
| 104 |
104 |
import com.goldencode.p2j.util.AppServerLauncher;
|
|
105 |
import com.goldencode.p2j.util.ConfigItem;
|
| 105 |
106 |
import com.goldencode.p2j.util.appserver.*;
|
| 106 |
107 |
import com.goldencode.p2j.util.logging.*;
|
| 107 |
108 |
|
| ... | ... | |
| 139 |
140 |
/** Timeout */
|
| 140 |
141 |
public static final int CLIENT_STARTUP_TIMEOUT_SEC = 30;
|
| 141 |
142 |
|
|
143 |
/**
|
|
144 |
* Smallest configured value not treated as a probable seconds-for-milliseconds mistake. The
|
|
145 |
* fastest client callback ever measured is over a second, so anything under this is far more
|
|
146 |
* likely to be a unit error than a deliberate setting.
|
|
147 |
*/
|
|
148 |
private static final long MIN_PLAUSIBLE_STARTUP_MS = 1000L;
|
|
149 |
|
|
150 |
/** Largest configured value not treated as a probable unit mistake (one hour). */
|
|
151 |
private static final long MAX_PLAUSIBLE_STARTUP_MS = 3_600_000L;
|
|
152 |
|
|
153 |
/**
|
|
154 |
* Effective client-startup timeout in MILLISECONDS, resolved once from the directory. Volatile
|
|
155 |
* because the first spawn to arrive resolves it and every later thread reads it.
|
|
156 |
*/
|
|
157 |
private static volatile long clientStartupTimeoutMs = -1L;
|
|
158 |
|
|
159 |
/**
|
|
160 |
* Milliseconds to wait for a spawned client to report ready.
|
|
161 |
* <p>
|
|
162 |
* Read from the directory node {@code clientStartupTimeout} (web client section), in
|
|
163 |
* MILLISECONDS for consistency with the sibling {@code pollTimeout} and
|
|
164 |
* {@code connectionTestTimeout} nodes. When the node is absent the built-in
|
|
165 |
* {@link #CLIENT_STARTUP_TIMEOUT_SEC} seconds default applies.
|
|
166 |
* </p>
|
|
167 |
* <p>
|
|
168 |
* Resolved once and reported at WARNING so the value in force is never in doubt, and a value
|
|
169 |
* that looks like the wrong unit is called out rather than applied silently: reading 120 as
|
|
170 |
* milliseconds would destroy every client instantly, and 120000 as seconds would mean nothing
|
|
171 |
* was ever reaped for 33 hours. Both failures are near-impossible to diagnose from behaviour.
|
|
172 |
* </p>
|
|
173 |
*
|
|
174 |
* @return The timeout in milliseconds.
|
|
175 |
*/
|
|
176 |
public static long getClientStartupTimeoutMs()
|
|
177 |
{
|
|
178 |
long cached = clientStartupTimeoutMs;
|
|
179 |
if (cached > 0L)
|
|
180 |
{
|
|
181 |
return cached;
|
|
182 |
}
|
|
183 |
|
|
184 |
long resolved = CLIENT_STARTUP_TIMEOUT_SEC * 1000L;
|
|
185 |
String source = "built-in default (directory node 'clientStartupTimeout' absent)";
|
|
186 |
try
|
|
187 |
{
|
|
188 |
Integer configured = ConfigItem.CLIENT_STARTUP_TIMEOUT.read(null);
|
|
189 |
if (configured != null && configured.intValue() > 0)
|
|
190 |
{
|
|
191 |
long ms = configured.longValue();
|
|
192 |
if (ms < MIN_PLAUSIBLE_STARTUP_MS)
|
|
193 |
{
|
|
194 |
LOG.log(Level.SEVERE,
|
|
195 |
"[SPAWN-CFG] directory node 'clientStartupTimeout'=%d is in MILLISECONDS "
|
|
196 |
+ "and %dms would destroy every client before it could start. This looks "
|
|
197 |
+ "like seconds; did you mean %d? Using the built-in %ds default instead.",
|
|
198 |
configured,
|
|
199 |
configured,
|
|
200 |
Long.valueOf(ms * 1000L),
|
|
201 |
Integer.valueOf(CLIENT_STARTUP_TIMEOUT_SEC));
|
|
202 |
}
|
|
203 |
else if (ms > MAX_PLAUSIBLE_STARTUP_MS)
|
|
204 |
{
|
|
205 |
LOG.log(Level.SEVERE,
|
|
206 |
"[SPAWN-CFG] directory node 'clientStartupTimeout'=%dms is over an hour, so "
|
|
207 |
+ "a client that never starts would never be reaped. Applying it as asked, "
|
|
208 |
+ "but check the unit -- this node is MILLISECONDS.",
|
|
209 |
configured);
|
|
210 |
resolved = ms;
|
|
211 |
source = "directory node 'clientStartupTimeout' (implausibly large)";
|
|
212 |
}
|
|
213 |
else
|
|
214 |
{
|
|
215 |
resolved = ms;
|
|
216 |
source = "directory node 'clientStartupTimeout'";
|
|
217 |
}
|
|
218 |
}
|
|
219 |
}
|
|
220 |
catch (Throwable t)
|
|
221 |
{
|
|
222 |
// A directory problem must not stop clients being spawned; fall back and say so.
|
|
223 |
LOG.log(Level.WARNING,
|
|
224 |
"[SPAWN-CFG] could not read 'clientStartupTimeout' from the directory, using "
|
|
225 |
+ "the built-in %ds default",
|
|
226 |
Integer.valueOf(CLIENT_STARTUP_TIMEOUT_SEC),
|
|
227 |
t);
|
|
228 |
}
|
|
229 |
|
|
230 |
clientStartupTimeoutMs = resolved;
|
|
231 |
LOG.log(Level.WARNING,
|
|
232 |
"[SPAWN-CFG] client startup timeout = %dms (%.1fs) (source: %s)",
|
|
233 |
Long.valueOf(resolved),
|
|
234 |
Double.valueOf(resolved / 1000.0),
|
|
235 |
source);
|
|
236 |
return resolved;
|
|
237 |
}
|
|
238 |
|
|
239 |
/**
|
|
240 |
* Wait for the spawned client to report ready, and make an expiry impossible to miss.
|
|
241 |
* <p>
|
|
242 |
* {@code CountDownLatch.await(timeout, unit)} returns false when the wait expired rather than
|
|
243 |
* the latch firing. Both call sites used to discard that boolean, so a client that never
|
|
244 |
* called back produced NO log at all -- the failure could only be inferred from a downstream
|
|
245 |
* symptom such as the browser being handed a dead port. It is now reported at SEVERE with the
|
|
246 |
* timeout that was applied and where that value came from.
|
|
247 |
* </p>
|
|
248 |
*
|
|
249 |
* @param where
|
|
250 |
* Call site, so the two waits are distinguishable in the log.
|
|
251 |
*
|
|
252 |
* @return {@code true} if the client reported ready, {@code false} if the wait expired.
|
|
253 |
*/
|
|
254 |
private boolean awaitClientStartup(String where)
|
|
255 |
{
|
|
256 |
long timeoutMs = getClientStartupTimeoutMs();
|
|
257 |
long start = System.nanoTime();
|
|
258 |
boolean signalled = false;
|
|
259 |
try
|
|
260 |
{
|
|
261 |
signalled = clientStartupCountDown.await(timeoutMs, TimeUnit.MILLISECONDS);
|
|
262 |
}
|
|
263 |
catch (InterruptedException ignored)
|
|
264 |
{
|
|
265 |
LOG.log(Level.WARNING,
|
|
266 |
"[SPAWN-TIMEOUT] %s: interrupted after %dms while waiting for client startup",
|
|
267 |
where,
|
|
268 |
Long.valueOf((System.nanoTime() - start) / 1_000_000L));
|
|
269 |
return false;
|
|
270 |
}
|
|
271 |
|
|
272 |
if (!signalled)
|
|
273 |
{
|
|
274 |
// The event this whole investigation could not see.
|
|
275 |
LOG.log(Level.SEVERE,
|
|
276 |
"[SPAWN-TIMEOUT] CLIENT STARTUP TIMEOUT at %s: no ready callback within %dms "
|
|
277 |
+ "(waited %dms) for osUser=%s -- the client is being DESTROYED even though it "
|
|
278 |
+ "may still be booting. Raise the directory node 'clientStartupTimeout' "
|
|
279 |
+ "(MILLISECONDS) if the client simply needs longer under load.",
|
|
280 |
where,
|
|
281 |
Long.valueOf(timeoutMs),
|
|
282 |
Long.valueOf((System.nanoTime() - start) / 1_000_000L),
|
|
283 |
getBuildParams().getOsUser());
|
|
284 |
}
|
|
285 |
return signalled;
|
|
286 |
}
|
|
287 |
|
| 142 |
288 |
/** Logger */
|
| 143 |
289 |
private static final CentralLogger LOG = CentralLogger.get(ClientSpawner.class);
|
| 144 |
290 |
|
| ... | ... | |
| 152 |
298 |
private volatile boolean ready = false;
|
| 153 |
299 |
|
| 154 |
300 |
/**
|
|
301 |
* System.nanoTime() at which the spawner process was forked, so clientIsReady() can report how
|
|
302 |
* long the client took to call back. Volatile: written by the spawning thread, read by the
|
|
303 |
* network thread that delivers the callback.
|
|
304 |
*/
|
|
305 |
private volatile long forkAtNanos = 0L;
|
|
306 |
|
|
307 |
/**
|
| 155 |
308 |
* Get {@link ClientBuilderParameters} used to spawn P2J clients.
|
| 156 |
309 |
*
|
| 157 |
310 |
* @return See above.
|
| ... | ... | |
| 323 |
476 |
int spawnerLaunchTimeout)
|
| 324 |
477 |
throws IOException
|
| 325 |
478 |
{
|
|
479 |
long tSpawnStart = System.nanoTime();
|
|
480 |
long allocNanos = 0L;
|
|
481 |
|
| 326 |
482 |
if (allocator != null)
|
| 327 |
483 |
{
|
| 328 |
484 |
ClientBuilderParameters buildParams = getBuildParams();
|
| ... | ... | |
| 330 |
486 |
"localhost",
|
| 331 |
487 |
buildParams.getUuid(),
|
| 332 |
488 |
buildParams.getOsUser());
|
|
489 |
allocNanos = System.nanoTime() - tSpawnStart;
|
|
490 |
|
| 333 |
491 |
if (allocatedResources == null)
|
| 334 |
492 |
{
|
|
493 |
// No free port. This is the port pool being exhausted, NOT a client that failed to
|
|
494 |
// start -- the two look identical from the browser and must not be conflated.
|
|
495 |
LOG.log(Level.WARNING,
|
|
496 |
"[SPAWN] ALLOC_FAIL osUser=%s | alloc=%dms | no free port in the pool",
|
|
497 |
buildParams.getOsUser(),
|
|
498 |
Long.valueOf(allocNanos / 1_000_000L));
|
| 335 |
499 |
return SpawnError.NO_AVAILABLE_PORTS_ERR_CODE.getCode();
|
| 336 |
500 |
}
|
| 337 |
|
|
|
501 |
|
| 338 |
502 |
((WebClientBuilder) getBuilder()).getParams().updateOptions(allocatedResources);
|
| 339 |
503 |
}
|
| 340 |
504 |
|
|
505 |
long tFork = System.nanoTime();
|
| 341 |
506 |
Process shell = getBuilder().localStart(environmentMap);
|
|
507 |
forkAtNanos = System.nanoTime();
|
|
508 |
long forkNanos = forkAtNanos - tFork;
|
| 342 |
509 |
|
| 343 |
510 |
isSpawnerRunning.set(true);
|
| 344 |
511 |
CentralLoggerServer.logSpawnerStream(shell.getErrorStream(), isSpawnerRunning);
|
| ... | ... | |
| 374 |
541 |
Future waitClientFuture = GENERIC_EXECUTOR.submit(() ->
|
| 375 |
542 |
{
|
| 376 |
543 |
// wait for notification on successful client start
|
| 377 |
|
try
|
| 378 |
|
{
|
| 379 |
|
clientStartupCountDown
|
| 380 |
|
.await(CLIENT_STARTUP_TIMEOUT_SEC, TimeUnit.SECONDS);
|
| 381 |
|
}
|
| 382 |
|
catch (InterruptedException ignored)
|
| 383 |
|
{
|
| 384 |
|
}
|
|
544 |
awaitClientStartup("spawnLocal");
|
| 385 |
545 |
});
|
| 386 |
546 |
|
| 387 |
547 |
Integer exitCode = null;
|
| ... | ... | |
| 395 |
555 |
LOG.log(Level.FINE, "Waiting for the exit code of the local spawn was unsuccessful.", e);
|
| 396 |
556 |
}
|
| 397 |
557 |
|
|
558 |
long waitNanos = System.nanoTime() - forkAtNanos;
|
|
559 |
|
| 398 |
560 |
if (!ready)
|
| 399 |
561 |
{
|
| 400 |
562 |
shell.destroy();
|
| 401 |
563 |
}
|
| 402 |
|
|
|
564 |
|
| 403 |
565 |
if (exitCode == null)
|
| 404 |
566 |
{
|
| 405 |
567 |
exitCode = SpawnError.GENERIC.getCode();
|
| 406 |
568 |
}
|
|
569 |
|
|
570 |
// The verdict line. exit==0 with ready==false means the spawner did its job and the client
|
|
571 |
// JVM never called back within CLIENT_STARTUP_TIMEOUT_SEC -- i.e. the failure is in client
|
|
572 |
// start-up, not in spawning, port allocation or account creation.
|
|
573 |
String outcome = ready ? "READY"
|
|
574 |
: (exitCode.intValue() == 0 ? "NOT_READY_CLIENT_SILENT" : "SPAWN_ERR");
|
|
575 |
LOG.log(Level.WARNING,
|
|
576 |
"[SPAWN] %s exit=%d ready=%b | alloc=%dms fork=%dms waitClient=%dms total=%dms "
|
|
577 |
+ "| clientStartupTimeoutMs=%d osUser=%s",
|
|
578 |
outcome,
|
|
579 |
exitCode,
|
|
580 |
Boolean.valueOf(ready),
|
|
581 |
Long.valueOf(allocNanos / 1_000_000L),
|
|
582 |
Long.valueOf(forkNanos / 1_000_000L),
|
|
583 |
Long.valueOf(waitNanos / 1_000_000L),
|
|
584 |
Long.valueOf((System.nanoTime() - tSpawnStart) / 1_000_000L),
|
|
585 |
Long.valueOf(getClientStartupTimeoutMs()),
|
|
586 |
getBuildParams().getOsUser());
|
|
587 |
|
| 407 |
588 |
return exitCode;
|
| 408 |
589 |
}
|
| 409 |
590 |
|
| ... | ... | |
| 439 |
620 |
else if (!ready)
|
| 440 |
621 |
{
|
| 441 |
622 |
// wait for notification on successful client start
|
| 442 |
|
try
|
| 443 |
|
{
|
| 444 |
|
clientStartupCountDown.await(CLIENT_STARTUP_TIMEOUT_SEC, TimeUnit.SECONDS);
|
| 445 |
|
}
|
| 446 |
|
catch (InterruptedException ignored)
|
| 447 |
|
{
|
| 448 |
|
}
|
|
623 |
awaitClientStartup("spawnRemote");
|
| 449 |
624 |
}
|
| 450 |
625 |
return exitCode;
|
| 451 |
626 |
}
|
| ... | ... | |
| 459 |
634 |
@Override
|
| 460 |
635 |
public void clientIsReady(String data)
|
| 461 |
636 |
{
|
|
637 |
long afterForkMs = forkAtNanos > 0L
|
|
638 |
? (System.nanoTime() - forkAtNanos) / 1_000_000L
|
|
639 |
: -1L;
|
|
640 |
|
|
641 |
// The exact moment the client became usable. Paired with [SPAWN], this separates "client
|
|
642 |
// was slow but arrived" from "client never arrived".
|
|
643 |
LOG.log(Level.WARNING,
|
|
644 |
"[SPAWN-CB] client called back afterForkMs=%d osUser=%s",
|
|
645 |
Long.valueOf(afterForkMs),
|
|
646 |
getBuildParams().getOsUser());
|
|
647 |
|
| 462 |
648 |
this.ready = true;
|
| 463 |
649 |
clientStartupCountDown.countDown();
|
| 464 |
650 |
}
|