Project

General

Profile

tempaccount-instrumented.patch

Șerban Bursuc, 08/14/2026 08:30 AM

Download (18.8 KB)

View differences:

new/src/com/goldencode/p2j/main/ClientSpawner.java
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
   }
new/src/com/goldencode/p2j/main/WebClientsManager.java
481 481
      }
482 482
      
483 483
      List<WebAllocatedResources> testedWebClientConfigs = new LinkedList<WebAllocatedResources>();
484
      
484

  
485
      long tAlloc = System.nanoTime();
486
      int probes = 0;
487

  
485 488
      int testPort = getTestPort(webClientConfig);
486 489
      InetAddress hostAddr = hostsManager.getResolvedHost(host);
487 490
      try
......
490 493
         {
491 494
            try
492 495
            {
496
               probes++;
493 497
               if (!Utils.isEndPointAvailable(hostAddr, testPort, connectionTestTimeout))
494 498
               {
499
                  // Nothing is listening:  the port is free and this is the one we will use.
495 500
                  break;
496 501
               }
497 502
            }
......
532 537
      
533 538
      if (webClientConfig == null)
534 539
      {
535
         LOG.log(Level.WARNING, "All available ports  are allocated - no config found.");
540
         LOG.log(Level.WARNING,
541
                 "[SPAWN-ALLOC] FAIL no free port after %d probe(s) in %dms (pool exhausted)",
542
                 Integer.valueOf(probes),
543
                 Long.valueOf((System.nanoTime() - tAlloc) / 1_000_000L));
536 544

  
537 545
         return null;
538 546
      }
547

  
548
      // Port secured.  `discarded` counts ports found already in use;  a large value means the
549
      // pool is filling up with clients that were allocated a port and never released it.
550
      LOG.log(Level.WARNING,
551
              "[SPAWN-ALLOC] port=%d probed=%d discarded=%d took=%dms",
552
              Integer.valueOf(webClientConfig.getPort()),
553
              Integer.valueOf(probes),
554
              Integer.valueOf(testedWebClientConfigs.size()),
555
              Long.valueOf((System.nanoTime() - tAlloc) / 1_000_000L));
539 556
      
540 557
      String webRoot = null;
541 558
      if (viaProxyServer)
......
561 578
         }
562 579
      };
563 580
      cleaners.put(uuid, cleaner);
564
      cleanerTimer.schedule(cleaner, ClientSpawner.CLIENT_STARTUP_TIMEOUT_SEC * 1000);
581
      // Must track the CONFIGURED timeout, not the built-in default:  a raised startup timeout
582
      // with a fixed 30s cleaner would free the port out from under a client that is still
583
      // booting, and the next allocation would hand the same port to someone else.
584
      cleanerTimer.schedule(cleaner, ClientSpawner.getClientStartupTimeoutMs());
565 585
      
566 586
      return webClientConfig;
567 587
   }
new/src/com/goldencode/p2j/main/WebDriverHandler.java
535 535
   public static Map<String, String[]> filterOutValidLoginParams(Fields fields)
536 536
   {
537 537
      Map<String, String[]> validatedParams = new HashMap<>();
538

  
539
      if (fields == null)
540
      {
541
         // The caller passes CLIENT_UUID_LOGIN_PARAMS.remove(uuid), which is null when the entry
542
         // was never registered or has already been consumed -- e.g. a client calling back after
543
         // its spawn was abandoned.  This used to NPE and kill the session;  report it and carry
544
         // on with no login params, which is what an unregistered client should get anyway.
545
         LOG.log(Level.WARNING,
546
                 "[SPAWN-CB] login params missing for a client callback (spawn abandoned, or a "
547
                 + "duplicate callback for the same uuid) -- continuing with none");
548
         return validatedParams;
549
      }
538 550
      
539 551
      for (String expectedParamName : expectedLoginParamNameRegex.keySet())
540 552
      {
......
1065 1077
      CLIENT_UUID_DEVICEID_PAIRS.put(clientUuid, spawnParameters.getDeviceId());
1066 1078
      CLIENT_UUID_LOGIN_PARAMS.put(clientUuid, requestParameters.getParamMap());
1067 1079

  
1080
      long tReq = System.nanoTime();
1068 1081
      int exitCode = spawner.spawn(webClientsManager,
1069 1082
                                   new String[] {
1070 1083
                                      requestParameters.getForwardedHost(),
1071 1084
                                      requestParameters.getForwardedProto(),
1072 1085
                                      requestParameters.getExternalUserIp()});
1086

  
1087
      // Outer boundary:  what the browser's POST /gui actually cost and how it ended.
1088
      LOG.log(Level.WARNING,
1089
              "[SPAWN-REQ] uuid=%s exit=%d totalMs=%d",
1090
              clientUuid,
1091
              Integer.valueOf(exitCode),
1092
              Long.valueOf((System.nanoTime() - tReq) / 1_000_000L));
1073 1093
      
1074 1094
      if (exitCode == 0)
1075 1095
      {
new/src/com/goldencode/p2j/util/ConfigItem.java
310 310
   public static final ConfigItem<String> HOST =
311 311
      new ConfigItem<>(String.class, "host", "client", "web", "host", Type.WEB_CLIENT);
312 312
   
313
   public static final ConfigItem<Integer> POLL_TIMEOUT = 
313
   public static final ConfigItem<Integer> POLL_TIMEOUT =
314 314
      new ConfigItem<>(Integer.class, "pollTimeout", "", "", "", Type.WEB_CLIENT);
315

  
316
   /**
317
    * MILLISECONDS to wait for a freshly spawned web client to report itself ready, before the
318
    * server gives up and destroys it.  Absent from the directory, {@code ClientSpawner}'s built-in
319
    * default applies.
320
    * <p>
321
    * Milliseconds, for consistency with the sibling nodes in this section ({@link #POLL_TIMEOUT},
322
    * {@link #CONNECTION_TEST_TIMEOUT}).  A value that looks like seconds is reported as a probable
323
    * unit mistake rather than silently applied -- see
324
    * ClientSpawner.getClientStartupTimeoutMs().
325
    * <p>
326
    * This bounds CLIENT BOOT, which under load has a long tail:  a value below the tail destroys
327
    * healthy clients mid-start-up and makes the caller retry, which adds load and lengthens the
328
    * tail further.  See ClientSpawner.getClientStartupTimeoutSec().
329
    * </p>
330
    */
331
   public static final ConfigItem<Integer> CLIENT_STARTUP_TIMEOUT =
332
      new ConfigItem<>(Integer.class, "clientStartupTimeout", "", "", "", Type.WEB_CLIENT);
315 333
   
316 334
   public static final ConfigItem<Integer> CONNECTION_TEST_TIMEOUT = 
317 335
      new ConfigItem<>(Integer.class, "connectionTestTimeout", "", "", "", Type.WEB_CLIENT);