Project

General

Profile

commit-guard-fastfind-r16812.patch

Constantin Asofiei, 10/29/2026 05:06 PM

Download (120 KB)

View differences:

src/com/goldencode/p2j/distributed/cache/CommitNotice.java
15 15
**                  the changes as other servers' (incrementRemoteIfTracked) and resolves the FastFindCache
16 16
**                  by database type and name (databaseType; the meta database has the primary's name).
17 17
**                  The per-database publish() is replaced by Pending; serialVersionUID 2.
18
**     CA  20261029 Removed the off mode (isEnabled()). A server which may have missed a notice clears its
19
**                  fast-find caches and counts a remote change of every cached record (missed()): a receiver
20
**                  which failed to apply it; the servers which did not confirm it in time, or all others
21
**                  when the send failed, through the Missed function (resync()); the survivors of a server
22
**                  which departed (LockMembershipHandler). The receiver no longer marks Geode's thread.
23
**                  An answer to a notice renews the answering server's membership lease (Confirmations).
18 24
*/
19 25

  
20 26
/*
......
105 111
 * there has locked a changed record and read it again from the database. It must not make that current copy
106 112
 * stale, or the session's update would not be saved: the receiver counts it apart from the local changes,
107 113
 * and a session does not treat as stale a copy read under a lock its context still holds (see
108
 * {@code Session.isStale}). A late notice therefore only delays NO-LOCK reads of other sessions there; a
109
 * notice which is lost (not delivered, or not applied: the receiver answers -1) leaves their cached copies
110
 * and FastFindCache entries of those records stale until they are evicted or change again. This
114
 * {@code Session.isStale}). A late notice therefore only delays NO-LOCK reads of other sessions there.
115
 * <p>
116
 * A server which may have missed a notice clears all its FastFindCache results and counts a remote change
117
 * of every record it caches ({@link #missed}), so nothing it cached before is used without reading it again:
118
 * <ul>
119
 *   <li>a receiver which could not apply a notice, at once (it answers -1);</li>
120
 *   <li>the servers which did not confirm a notice in time (the sender's wait timed out, they left during
121
 *       it, or the wait failed), or every other server when the send itself failed: the sender tells them
122
 *       ({@link Missed}, without waiting; the servers to tell are coalesced while a call is pending); a
123
 *       server which is unresponsive gets it when it answers again, or Geode removes it from the view and
124
 *       its re-initialisation clears; a server which answers but cannot run it logs that;</li>
125
 *   <li>every server, when another FWD server departs (it may have committed without sending, or while
126
 *       sending, its notices), once the departed server can no longer commit ({@code LockMembershipHandler},
127
 *       after {@code MembershipLease.GRACE_MS});</li>
128
 *   <li>a server which lost its membership, in its re-initialisation ({@code GeodeConfig}).</li>
129
 * </ul>
130
 * In the batch and async modes a notice which is sent but lost without an error is not noticed. This
111 131
 * needs a lock manager which tells since which value of the JVM-wide sequence (no clock) a lock is held
112 132
 * ({@code LockManager.heldSinceSeq}): the owner-side lock manager, the cluster default; with another one a
113 133
 * late notice can still drop a lock holder's update. Prerequisite of the cluster mode: the database runs at
......
135 155
 * to each other server, waited for (sync), or at most one per {@link #BATCH_MS} (batch); the message holds
136 156
 * the changed records' ids. This replaces the FastFindCache's own broadcasts (several per transaction).
137 157
 * <p>
138
 * The mode is {@link #MODE}: {@code sync} (the default), {@code batch}, {@code async} (one call per
139
 * transaction, not waited for) or {@code off}.
158
 * The mode is {@link #MODE}: {@code sync} (the default), {@code batch} or {@code async} (one call per
159
 * transaction, not waited for). The commit notice is the only invalidation of the FastFindCache across the
160
 * servers.
140 161
 */
141 162
public final class CommitNotice
142 163
implements Serializable
......
150 171
   /** How long a sender waits for the other servers to apply its notice, in ms (sync mode). */
151 172
   private static final long TIMEOUT_MS = 10000L;
152 173

  
153
   /** The mode: sync (the default), batch, async or off. */
174
   /** The mode: sync (the default), batch or async. */
154 175
   private static final String MODE = "sync";
155 176

  
177
   /** The delay (ms) of a call of {@link #resync}, so that the servers named meanwhile are told together. */
178
   private static final long RESYNC_DELAY_MS = 1000L;
179

  
156 180
   /** Batch mode: the least ms between two sends of a server. */
157 181
   private static final long BATCH_MS = 5L;
158 182

  
......
170 194
   /** Batch mode: the sender thread, created on first use (guarded by {@link #batchPending}). */
171 195
   private static ScheduledExecutorService flusher = null;
172 196

  
197
   /** The thread of {@link #resync}, created on first use (guarded by {@link #batchPending}). */
198
   private static ScheduledExecutorService resyncer = null;
199

  
200
   /** The servers {@link #resync} tells in its next call. */
201
   private static final Set<DistributedMember> resyncPending = new HashSet<>();
202

  
203
   /** {@link #resync}'s next call tells every other server (guarded by {@link #resyncPending}). */
204
   private static boolean resyncAll = false;
205

  
206
   /** A call of {@link #resync} is scheduled and has not taken the pending servers yet (guarded likewise). */
207
   private static boolean resyncScheduled = false;
208

  
173 209
   /** The cluster key of the {@link DmoVersioning} of the database and tenant. */
174 210
   private final String versioningKey;
175 211

  
......
213 249
   }
214 250

  
215 251
   /**
216
    * Check whether the servers send commit notices.
252
    * This server may have missed commit notices of the other servers (see the class description): clear
253
    * every result of its FastFindCaches and count a remote change of every record it tracks, so that every
254
    * cached copy is read again before it is used.
217 255
    *
218
    * @return   {@code true} unless the mode is {@code off}.
256
    * @param    why
257
    *           Why, for the log.
219 258
    */
220
   public static boolean isEnabled()
259
   public static void missed(String why)
221 260
   {
222
      return !"off".equals(MODE);
261
      LOG.warning("Commit notices may have been missed (" + why + "): the fast-find caches are cleared and " +
262
                  "the cached records are read again");
263
      FastFindCache.invalidateAllPermanent();
264
      DmoVersioning.incrementRemoteAll();
223 265
   }
224 266

  
225 267
   /**
......
274 316
    * as the mode says: in sync mode (the default) in one call, and wait once (at most {@link #TIMEOUT_MS})
275 317
    * until every other server has applied them; a server which does not answer in time or an error is
276 318
    * logged and the commit goes on (Geode's wait is not interruptible; its interrupt case is defensive).
277
    * Batch mode queues them, async mode sends them without waiting, off sends nothing.
319
    * The servers which did not confirm the notices, or all other servers if the send failed, are told that
320
    * they missed them ({@link #resync}). Batch mode queues them, async mode sends them without waiting (a
321
    * failed send is told to all other servers too).
278 322
    *
279 323
    * @param    notices
280 324
    *           The notices; not empty.
281 325
    */
282 326
   private static void send(CommitNotice[] notices)
283 327
   {
284
      if ("off".equals(MODE))
285
      {
286
         return;
287
      }
288

  
289 328
      if ("batch".equals(MODE))
290 329
      {
291 330
         enqueue(notices);
292 331
         return;
293 332
      }
294 333

  
334
      Set<DistributedMember> others = null;
335
      Confirmations confirmed = new Confirmations();
295 336
      try
296 337
      {
297 338
         // inside the try: on a server whose cache is closed (expelled) this fails, and the commit goes on
298
         Set<DistributedMember> others = GeodeConfig.getOtherServerMembers();
339
         others = GeodeConfig.getOtherServerMembers();
299 340
         if (others.isEmpty())
300 341
         {
301 342
            return;
302 343
         }
303 344

  
304
         ResultCollector<?, ?> rc = FunctionService.onMembers(others)
305
                                                   .setArguments(notices)
306
                                                   .execute(Receiver.ID);
307
         if ("sync".equals(MODE))
308
         {
309
            // one wait for all the other servers, which apply the notices in parallel
310
            Object results = rc.getResult(TIMEOUT_MS, TimeUnit.MILLISECONDS);
311
            if (results instanceof List && ((List<?>) results).contains(-1))
312
            {
313
               LOG.warning("Commit notice of " + describe(notices) + " not applied by every server (see " +
314
                           "their log)");
315
            }
345
         ResultCollector<Object, Object> rc = FunctionService.onMembers(others)
346
                                                             .setArguments(notices)
347
                                                             .withCollector(confirmed)
348
                                                             .execute(Receiver.ID);
349
         if (!"sync".equals(MODE))
350
         {
351
            return;
352
         }
353

  
354
         // one wait for all the other servers, which apply the notices in parallel
355
         rc.getResult(TIMEOUT_MS, TimeUnit.MILLISECONDS);
356
         if (confirmed.failed())
357
         {
358
            LOG.warning("Commit notice of " + describe(notices) + " not applied by every server (see " +
359
                        "their log)");
316 360
         }
317 361
      }
318 362
      catch (InterruptedException exc)
......
326 370
         LOG.warning("Commit notice of " + describe(notices) + " not confirmed by every server: " + exc);
327 371
         LOG.log(Level.FINE, "Commit notice not confirmed", exc);
328 372
      }
373

  
374
      // the servers which did not answer may have missed the notices (one which answered -1 cleared itself);
375
      // in async mode, only a failed send is known
376
      Set<DistributedMember> missing = (others == null) ? null : confirmed.missing(others);
377
      if (missing == null || !missing.isEmpty())
378
      {
379
         resync(missing, "commit notice of " + describe(notices) + " not confirmed");
380
      }
381
   }
382

  
383
   /**
384
    * Tell other servers that they may have missed commit notices: they clear what they cached
385
    * ({@link #missed}). Sent on a daemon thread without waiting for an answer; the servers named while a call
386
    * is pending are told in the next call, once each (so a series of timed-out notices makes a server clear
387
    * once, not once per notice): a call starts {@link #RESYNC_DELAY_MS} ms after the first server was named,
388
    * so that the notices timing out during one stall are told together. If this server is not a member of
389
    * the cluster any more the call fails, which is logged (its departure makes the others clear).
390
    *
391
    * @param    members
392
    *           The servers, or {@code null} for every other server.
393
    * @param    why
394
    *           Why, for their log.
395
    */
396
   private static void resync(Set<DistributedMember> members, String why)
397
   {
398
      synchronized (resyncPending)
399
      {
400
         if (members == null)
401
         {
402
            resyncAll = true;
403
         }
404
         else
405
         {
406
            resyncPending.addAll(members);
407
         }
408

  
409
         if (resyncScheduled)
410
         {
411
            return;
412
         }
413
         resyncScheduled = true;
414
      }
415

  
416
      resyncer().schedule(() ->
417
      {
418
         Set<DistributedMember> to;
419
         synchronized (resyncPending)
420
         {
421
            to = resyncAll ? GeodeConfig.getOtherServerMembers() : new HashSet<>(resyncPending);
422
            resyncPending.clear();
423
            resyncAll = false;
424
            resyncScheduled = false;
425
         }
426

  
427
         try
428
         {
429
            if (!to.isEmpty())
430
            {
431
               FunctionService.onMembers(to).setArguments(why).execute(Missed.ID);
432
               LOG.warning("Commit notices: told " + to.size() + " server(s) that they may have missed " +
433
                           "notices (" + why + ")");
434
            }
435
         }
436
         catch (RuntimeException exc)
437
         {
438
            LOG.warning("Commit notices: could not tell the other servers that they may have missed " +
439
                        "notices (" + why + "): " + exc);
440
         }
441
      }, RESYNC_DELAY_MS, TimeUnit.MILLISECONDS);
329 442
   }
330 443

  
331 444
   /**
......
392 505
      catch (Exception exc)
393 506
      {
394 507
         LOG.log(Level.WARNING, "Commit notice batch not sent: " + exc, exc);
508
         resync(null, "commit notice batch not sent");
395 509
      }
396 510
   }
397 511

  
......
418 532
   }
419 533

  
420 534
   /**
535
    * The daemon thread which tells other servers that they may have missed notices ({@link #resync}).
536
    *
537
    * @return   The executor, created on first use.
538
    */
539
   private static ScheduledExecutorService resyncer()
540
   {
541
      synchronized (batchPending)
542
      {
543
         if (resyncer == null)
544
         {
545
            resyncer = Executors.newSingleThreadScheduledExecutor(r ->
546
            {
547
               Thread t = new Thread(r, "FWD commit notice resync");
548
               t.setDaemon(true);
549
               return t;
550
            });
551
         }
552
         return resyncer;
553
      }
554
   }
555

  
556
   /**
421 557
    * Apply the notice to the record versions of this server.
422 558
    *
423 559
    * @return   The number of records whose version this server tracked.
......
460 596
               dmoIds.add(FastFindCache.combine(dmoMeta.getId(), null));
461 597
            }
462 598
         }
463
         // a remote request: invalidate here only, never broadcast again
464
         FastFindCache.setIsFromRemoteRequest();
465 599
         ffCache.invalidate(dmoIds);
466 600
      }
467 601

  
......
541 675
       * Apply the notices (one per database and tenant) and answer the number of tracked records. The record
542 676
       * versions of every notice are applied first, then the FastFindCache invalidations, each notice on its
543 677
       * own, so a failure leaves the others applied. Always answers: an exception sent back to the sender
544
       * would end its wait for every server, not only this one; a failure is logged and answered with -1.
678
       * would end its wait for every server, not only this one; a failure is logged, this server clears what
679
       * it cached ({@link CommitNotice#missed}), and it answers -1.
545 680
       *
546 681
       * @param    context
547 682
       *           The function context, whose argument is the notices.
......
576 711
               notApplied(notice, exc);
577 712
            }
578 713
         }
714
         if (failed)
715
         {
716
            try
717
            {
718
               missed("a commit notice could not be applied on this server");
719
            }
720
            catch (RuntimeException | LinkageError | AssertionError exc)
721
            {
722
               // always answer (see above)
723
               LOG.warning("Fast-find caches not cleared after a failed commit notice: " + exc);
724
            }
725
         }
579 726
         context.getResultSender().lastResult(failed ? -1 : tracked);
580 727
      }
581 728

  
......
639 786
         LOG.log(Level.FINE, "Commit notice not applied", exc);
640 787
      }
641 788
   }
789

  
790
   /**
791
    * The collector of the answers to a notice in sync mode (Geode waits for them, see
792
    * {@link ResultCollector#getResult(long, TimeUnit)}): it records which servers answered, so that the
793
    * others can be told that they may have missed the notice.
794
    */
795
   private static final class Confirmations
796
   implements ResultCollector<Object, Object>
797
   {
798
      /** {@code System.nanoTime()} when the collector was made (right before the notice was sent). */
799
      private final long sentNanos = System.nanoTime();
800

  
801
      /** {@code System.currentTimeMillis()} likewise. */
802
      private final long sentWall = System.currentTimeMillis();
803

  
804
      /** The cluster gate's epoch likewise. */
805
      private final long epoch = ClusterGate.epoch();
806

  
807
      /** The servers which answered. */
808
      private final Set<DistributedMember> answered = ConcurrentHashMap.newKeySet();
809

  
810
      /** The answers. */
811
      private final List<Object> results = Collections.synchronizedList(new ArrayList<>());
812

  
813
      /** {@code true} if a server answered that it could not apply the notice (-1). */
814
      private volatile boolean failed = false;
815

  
816
      /**
817
       * A server answered (an exception, e.g. for a server which left, is no confirmation).
818
       *
819
       * @param    member
820
       *           The server.
821
       * @param    result
822
       *           Its answer.
823
       */
824
      @Override
825
      public void addResult(DistributedMember member, Object result)
826
      {
827
         results.add(result);
828
         if (result instanceof Throwable)
829
         {
830
            return;
831
         }
832

  
833
         if (Integer.valueOf(-1).equals(result))
834
         {
835
            failed = true;
836
         }
837
         answered.add(member);
838

  
839
         // an answer also shows that the server still counts this one a member (its membership lease)
840
         MembershipLease.answered(member, sentNanos, sentWall, epoch);
841
      }
842

  
843
      /**
844
       * The answers so far.
845
       *
846
       * @return   The answers.
847
       */
848
      @Override
849
      public Object getResult()
850
      {
851
         return results;
852
      }
853

  
854
      /**
855
       * The answers so far (Geode's execution waits for them before it calls this).
856
       *
857
       * @param    timeout
858
       *           Ignored.
859
       * @param    unit
860
       *           Ignored.
861
       *
862
       * @return   The answers.
863
       */
864
      @Override
865
      public Object getResult(long timeout, TimeUnit unit)
866
      {
867
         return results;
868
      }
869

  
870
      /**
871
       * Every server answered: nothing to do.
872
       */
873
      @Override
874
      public void endResults()
875
      {
876
      }
877

  
878
      /**
879
       * Forget the answers (Geode calls this before it executes again; notices are not re-executed).
880
       */
881
      @Override
882
      public void clearResults()
883
      {
884
         results.clear();
885
         answered.clear();
886
         failed = false;
887
      }
888

  
889
      /**
890
       * Whether a server answered that it could not apply the notice.
891
       *
892
       * @return   {@code true} if one did (it cleared what it cached).
893
       */
894
      boolean failed()
895
      {
896
         return failed;
897
      }
898

  
899
      /**
900
       * The servers which did not answer.
901
       *
902
       * @param    sentTo
903
       *           The servers the notice was sent to.
904
       *
905
       * @return   Those of them which did not answer (a new set).
906
       */
907
      Set<DistributedMember> missing(Set<DistributedMember> sentTo)
908
      {
909
         Set<DistributedMember> missing = new HashSet<>(sentTo);
910
         missing.removeAll(answered);
911

  
912
         return missing;
913
      }
914
   }
915

  
916
   /**
917
    * The Geode function which tells a server that it may have missed commit notices
918
    * ({@link CommitNotice#resync}): it clears what it cached ({@link CommitNotice#missed}).
919
    */
920
   public static class Missed
921
   implements Function<String>
922
   {
923
      /** The function id. */
924
      static final String ID = Missed.class.getName();
925

  
926
      /** Serial version. */
927
      private static final long serialVersionUID = 1L;
928

  
929
      /**
930
       * Clear what this server cached.
931
       *
932
       * @param    context
933
       *           The function context, whose argument tells why.
934
       */
935
      @Override
936
      public void execute(FunctionContext<String> context)
937
      {
938
         missed("told by another server: " + context.getArguments());
939
      }
940

  
941
      /**
942
       * The function id.
943
       *
944
       * @return   {@link #ID}.
945
       */
946
      @Override
947
      public String getId()
948
      {
949
         return ID;
950
      }
951

  
952
      /**
953
       * The sender does not wait for an answer.
954
       *
955
       * @return   {@code false}.
956
       */
957
      @Override
958
      public boolean hasResult()
959
      {
960
         return false;
961
      }
962

  
963
      /**
964
       * Not sent again to another member when a member fails.
965
       *
966
       * @return   {@code false}.
967
       */
968
      @Override
969
      public boolean isHA()
970
      {
971
         return false;
972
      }
973

  
974
      /**
975
       * Not a data-dependent function.
976
       *
977
       * @return   {@code false}.
978
       */
979
      @Override
980
      public boolean optimizeForWrite()
981
      {
982
         return false;
983
      }
984
   }
642 985
}
643
-- a/src/com/goldencode/p2j/distributed/config/ClusterGate.java
986
++ b/src/com/goldencode/p2j/distributed/config/ClusterGate.java
......
10 10
**                  every session is terminated, logins and appserver requests are refused until the
11 11
**                  in-process re-initialisation after Geode's reconnect, through participants, is done;
12 12
**                  the appserver agents are started again after it).
13
**     CA  20261029 Commit fence (enterCommit()/exitCommit()): closing() refuses new commits and waits for
14
**                  those in flight before the cache closes; started() starts the MembershipLease pinger.
13 15
*/
14 16

  
15 17
/*
......
191 193
    */
192 194
   private static final long MSA_UNWIND_MS = 30_000L;
193 195

  
196
   /** The longest wait (ms) of {@link #closing} for the commits in flight ({@link #enterCommit}). */
197
   private static final long FENCE_WAIT_MS = 10_000L;
198

  
199
   /** Guards {@link #commitsInFlight} and {@link #fenceClosed}; {@link #closing} waits on it. */
200
   private static final Object FENCE = new Object();
201

  
194 202
   /** Session ids above this one are local ({@link #localSessionId}); the cluster's sequence stays below. */
195 203
   private static final int LOCAL_SESSION_ID_FLOOR = 2_000_000_000;
196 204

  
......
252 260
    */
253 261
   private static volatile boolean rejoinFailed = false;
254 262

  
263
   /** The number of commits between {@link #enterCommit} and {@link #exitCommit} (guarded by FENCE). */
264
   private static int commitsInFlight = 0;
265

  
266
   /** {@code true} once {@link #closing} started: no commit enters any more (guarded by FENCE). */
267
   private static boolean fenceClosed = false;
268

  
255 269
   /** The re-initialisation participants, kept sorted by their order. */
256 270
   private static final List<Registered> PARTICIPANTS = new CopyOnWriteArrayList<>();
257 271

  
......
604 618
   }
605 619

  
606 620
   /**
621
    * Enter the commit fence before the commit of a database transaction ({@code TxWrapper}), before every
622
    * other check of the commit; {@link #exitCommit} must follow once the database commit is done or failed.
623
    * While a commit is inside the fence, FWD's shutdown does not close the cache ({@link #closing} waits for
624
    * it), so its locks are still held by this member when its change reaches the database; once the shutdown
625
    * started, no commit enters.
626
    *
627
    * @param   db
628
    *          The database whose transaction commits.
629
    *
630
    * @throws  DatabaseConnectionException
631
    *          if FWD's shutdown closes the cache (the database transaction is rolled back).
632
    */
633
   public static void enterCommit(Database db)
634
   {
635
      synchronized (FENCE)
636
      {
637
         if (!fenceClosed)
638
         {
639
            commitsInFlight++;
640
            return;
641
         }
642
      }
643

  
644
      throw (DatabaseConnectionException) unavailable(db, null);
645
   }
646

  
647
   /**
648
    * Leave the commit fence entered by {@link #enterCommit}.
649
    */
650
   public static void exitCommit()
651
   {
652
      synchronized (FENCE)
653
      {
654
         commitsInFlight--;
655
         if (commitsInFlight <= 0)
656
         {
657
            FENCE.notifyAll();
658
         }
659
      }
660
   }
661

  
662
   /**
607 663
    * Build the STOP of the gate.
608 664
    *
609 665
    * @param   db
......
723 779
      t.start();
724 780

  
725 781
      LOG.log(Level.INFO, "Cluster gate: running (watchdog every " + WATCH_MS + " ms)");
782

  
783
      MembershipLease.start(c);
726 784
   }
727 785

  
728 786
   /**
729 787
    * FWD closes the cache (server shutdown): the gate is {@link State#CLOSED} from now on, so code needing
730 788
    * the cluster gets STOP; the epoch is kept until the cache is closed ({@link #closed}), so that the
731
    * releases of the sessions still reach the owners meanwhile. Nobody is interrupted. Called by
732
    * {@link GeodeConfig}'s shutdown hook before it closes the cache.
789
    * releases of the sessions still reach the owners meanwhile. Nobody is interrupted. No commit enters the
790
    * commit fence any more, and the commits in it are waited for (at most {@link #FENCE_WAIT_MS} ms), so that
791
    * none reaches its database after the cache closed and the other members purged this member's locks.
792
    * Called by {@link GeodeConfig}'s shutdown hook before it closes the cache.
733 793
    */
734 794
   static void closing()
735 795
   {
......
754 814
      }
755 815

  
756 816
      LOG.log(Level.INFO, "Cluster gate: closed (the server shuts down)");
817

  
818
      awaitCommits();
757 819
   }
758 820

  
759 821
   /**
......
810 872
   }
811 873

  
812 874
   /**
875
    * Close the commit fence and wait (at most {@link #FENCE_WAIT_MS} ms) until no commit is in it; an
876
    * interrupt ends the wait and is kept.
877
    */
878
   private static void awaitCommits()
879
   {
880
      boolean interrupted = false;
881
      int left;
882
      synchronized (FENCE)
883
      {
884
         fenceClosed = true;
885
         long end = System.nanoTime() + FENCE_WAIT_MS * 1_000_000L;
886
         long ms;
887
         while (commitsInFlight > 0 && !interrupted && (ms = (end - System.nanoTime()) / 1_000_000L) > 0)
888
         {
889
            try
890
            {
891
               FENCE.wait(ms);
892
            }
893
            catch (InterruptedException exc)
894
            {
895
               interrupted = true;
896
            }
897
         }
898
         left = commitsInFlight;
899
      }
900

  
901
      if (interrupted)
902
      {
903
         Thread.currentThread().interrupt();
904
      }
905

  
906
      if (left > 0)
907
      {
908
         LOG.log(Level.WARNING, "Cluster gate: " + left + " commit(s) still in flight when the cache closes");
909
      }
910
   }
911

  
912
   /**
813 913
    * Whether a cache is closed or closing.
814 914
    *
815 915
    * @param   c
816
-- a/src/com/goldencode/p2j/distributed/config/GeodeConfig.java
916
++ b/src/com/goldencode/p2j/distributed/config/GeodeConfig.java
......
28 28
**                  re-initialisation participants: the reconnected cache is adopted and FWD's functions
29 29
**                  registered again (registerFunctions()), ManagerClasses re-checked, the rebalance
30 30
**                  coordinator started on it, the fast-find caches cleared and the record versions bumped.
31
**     CA  20261029 Registered MembershipLease.Ping/Check and CommitNotice.Missed; removed the registration of
32
**                  FastFindCacheInvalidationFunction; missedNotices() goes through CommitNotice.missed();
33
**                  added isServerMember().
31 34
*/
32 35

  
33 36
/*
......
279 282
   private static void registerFunctions()
280 283
   {
281 284
      FunctionService.registerFunction(new GetP2JJarVersionFunction());
282
      FunctionService.registerFunction(new FastFindCacheInvalidationFunction());
283 285
      FunctionService.registerFunction(new com.goldencode.p2j.distributed.cache.CommitNotice.Receiver());
286
      FunctionService.registerFunction(new com.goldencode.p2j.distributed.cache.CommitNotice.Missed());
287
      FunctionService.registerFunction(new MembershipLease.Ping());
288
      FunctionService.registerFunction(new MembershipLease.Check());
284 289
      FunctionService.registerFunction(new TenantRegistryReloadFunction());
285 290
      FunctionService.registerFunction(new com.goldencode.p2j.distributed.lock.OwnerLockFunction());
286 291
      FunctionService.registerFunction(new com.goldencode.p2j.distributed.lock.TableLeases.RevokeFunction());
......
334 339
    */
335 340
   private static void missedNotices()
336 341
   {
337
      FastFindCache.invalidateAllPermanent();
338
      DmoVersioning.incrementRemoteAll();
342
      com.goldencode.p2j.distributed.cache.CommitNotice.missed("this server was not a member of the cluster");
339 343
   }
340 344

  
341 345
   /**
......
546 550
   }
547 551

  
548 552
   /**
553
    * Whether a member of the distributed system is an FWD server (not, e.g., a locator).
554
    *
555
    * @param   member
556
    *          The member.
557
    *
558
    * @return  {@code true} if it belongs to the FWD server group.
559
    */
560
   public static boolean isServerMember(DistributedMember member)
561
   {
562
      return member != null && member.getGroups().contains(FWD_SERVER_GROUP);
563
   }
564

  
565
   /**
549 566
    * Constructs the locator string in the format "host[port]".
550 567
    * 
551 568
    * @return  A string formatted for Geode's "locators" property.
552
-- a/src/com/goldencode/p2j/distributed/lock/LiveMembers.java
569
++ b/src/com/goldencode/p2j/distributed/lock/LiveMembers.java
......
8 8
** 001 ES  20261026 Initial version (#4369: liveness checks without a member set built per operation).
9 9
**     CA  20261028 The view is built from GeodeConfig's cache, not CacheFactory.getAnyInstance(), which waits
10 10
**                  for Geode's reconnect and then answers the reconnected cache.
11
**     CA  20261029 holdsLocks()/lockerChecker(): a departed member's locks and reservations still stand for
12
**                  MembershipLease.GRACE_MS after this member learned it is gone (GONE).
11 13
*/
12 14

  
13 15
/*
......
91 93
 *       not list it: a member missing from the cached view is checked against the current view before the
92 94
 *       answer, so a live member is never purged or refused because of a stale cache.</li>
93 95
 * </ul>
96
 * The locks and unique key reservations of a member which is gone still stand for
97
 * {@link MembershipLease#GRACE_MS} ms after this member learned of it ({@link #holdsLocks},
98
 * {@link #lockerChecker}): a member paused past the member timeout may still be committing under them until
99
 * its membership lease expired. A departed member is never granted anything new ({@link #isLive},
100
 * {@link #checker}, {@link #hasLeft} have no grace).
94 101
 */
95 102
public final class LiveMembers
96 103
{
......
100 107
   /** Time (ms) a member whose departure was reported is remembered. */
101 108
   private static final long DEPARTED_KEEP_MS = 3_600_000L;
102 109

  
110
   /** Size of {@link #GONE} above which its old entries are dropped at the next grace check. */
111
   private static final int GONE_PRUNE_SIZE = 256;
112

  
103 113
   /** The view answering while the cache is closing: every member live, so nothing is purged. */
104 114
   private static final View CLOSING = new View(null, null, Collections.emptySet(), 0L);
105 115

  
106 116
   /** Members whose departure the membership listener reported, with the time of the report (ms). */
107 117
   private static final ConcurrentHashMap<String, Long> DEPARTED = new ConcurrentHashMap<>();
108 118

  
119
   /**
120
    * Members known to be gone (departure reported, or missing from a current view), with the time this
121
    * member learned it ({@code System.nanoTime()}); for the grace of their locks ({@link #holdsLocks}).
122
    */
123
   private static final ConcurrentHashMap<String, Long> GONE = new ConcurrentHashMap<>();
124

  
109 125
   /** The cached view; {@code null} to build a new one at the next check. */
110 126
   private static volatile View view = null;
111 127

  
......
154 170
   }
155 171

  
156 172
   /**
173
    * Whether the locks and unique key reservations a member holds still stand: it is live ({@link #isLive}),
174
    * or it is gone since less than {@link MembershipLease#GRACE_MS} ms (see the class description).
175
    *
176
    * @param   member
177
    *          Member id.
178
    *
179
    * @return  {@code true} if they do.
180
    */
181
   public static boolean holdsLocks(String member)
182
   {
183
      return isLive(member) || inGrace(member);
184
   }
185

  
186
   /**
187
    * A check for several lockers in one decision, as {@link #checker} (one view at most) but with the grace
188
    * of {@link #holdsLocks}: whether a locker's locks still stand.
189
    *
190
    * @return  The check; for one decision only (not thread safe).
191
    */
192
   public static Predicate<String> lockerChecker()
193
   {
194
      Predicate<String> live = checker();
195

  
196
      return member -> live.test(member) || inGrace(member);
197
   }
198

  
199
   /**
157 200
    * Whether the membership listener reported the departure of a member (a definite answer: member ids are
158 201
    * not reused).
159 202
    *
......
193 236
      long now = System.currentTimeMillis();
194 237
      DEPARTED.put(member, now);
195 238
      DEPARTED.values().removeIf(t -> now - t > DEPARTED_KEEP_MS);
239
      long nanos = System.nanoTime();
240
      GONE.put(member, nanos);
241
      GONE.values().removeIf(t -> nanos - t > DEPARTED_KEEP_MS * 1_000_000L);
196 242
      view = null;
197 243
   }
198 244

  
......
205 251
   }
206 252

  
207 253
   /**
254
    * Whether a member which is not live is gone since less than {@link MembershipLease#GRACE_MS} ms (from
255
    * the time this member learned it; the first time is noted here if the departure was not reported).
256
    *
257
    * @param   member
258
    *          Member id, of a member which is not live.
259
    *
260
    * @return  {@code true} if it is.
261
    */
262
   private static boolean inGrace(String member)
263
   {
264
      long now = System.nanoTime();
265
      long since = GONE.computeIfAbsent(member, m -> now);
266
      if (GONE.size() > GONE_PRUNE_SIZE)
267
      {
268
         GONE.values().removeIf(t -> now - t > DEPARTED_KEEP_MS * 1_000_000L);
269
      }
270

  
271
      return now - since < MembershipLease.GRACE_MS * 1_000_000L;
272
   }
273

  
274
   /**
208 275
    * The cached view, or a new one if there is none, it is older than {@link #TTL_MS}, or its distributed
209 276
    * system is not connected any more (e.g. reconnected with a new one).
210 277
    *
211
-- a/src/com/goldencode/p2j/distributed/lock/LockMembershipHandler.java
278
++ b/src/com/goldencode/p2j/distributed/lock/LockMembershipHandler.java
......
9 9
**     ES  20261026 No purge while this member's cache is closing; a failed purge never reaches the uncaught
10 10
**                  exception handler; membership events update LiveMembers.
11 11
**     CA  20261029 Added reregister(): the listener is registered again on Geode's reconnected cache.
12
**     CA  20261029 When an FWD server departs, its commit notices may be lost: once its membership lease
13
**                  expired, this server clears what it cached (CommitNotice.missed()).
12 14
*/
13 15

  
14 16
/*
......
153 155
   @Override
154 156
   public void memberLeft(MembershipEvent event)
155 157
   {
156
      departed(event.getMemberId(), "left");
158
      departed(event, "left");
157 159
   }
158 160

  
159 161
   /**
......
165 167
   @Override
166 168
   public void memberCrashed(MembershipEvent event)
167 169
   {
168
      departed(event.getMemberId(), "crashed");
170
      departed(event, "crashed");
169 171
   }
170 172

  
171 173
   /**
172
    * Purge the locks and unique key reservations of a departed member, on separate threads (each makes a
173
    * second pass later).
174
    * Purge the locks and unique key reservations of a departed member, on separate threads (each waits for
175
    * the grace of the member's membership lease first, and makes a second pass later). When the member is an
176
    * FWD server, its commit notices of the last moments may be lost (it committed and did not send them, or
177
    * was sending them): once it can no longer commit (the same grace), this server clears what it cached
178
    * ({@code CommitNotice.missed}).
174 179
    *
175
    * @param   memberId
176
    *          Id of the departed member.
180
    * @param   event
181
    *          The membership event, naming the departed member.
177 182
    * @param   how
178 183
    *          Description of the departure.
179 184
    */
180
   private void departed(String memberId, String how)
185
   private void departed(MembershipEvent event, String how)
181 186
   {
187
      String memberId = event.getMemberId();
182 188
      LiveMembers.departed(memberId);
183 189

  
184 190
      if (OwnerLockManager.cacheClosing())
......
193 199
      start(() -> com.goldencode.p2j.distributed.orm.OwnerUniqueIndex.purgeDepartedMember(memberId),
194 200
            "Owner-Unique-Purge",
195 201
            memberId);
202
      if (GeodeConfig.isServerMember(event.getDistributedMember()))
203
      {
204
         start(() -> missedNotices(memberId, how), "Commit-Notice-Missed", memberId);
205
      }
206
   }
207

  
208
   /**
209
    * Clear what this server cached once the grace of a departed server's membership lease ended (see
210
    * {@link #departed}); nothing if this member's cache is closing meanwhile (its own re-initialisation
211
    * clears).
212
    *
213
    * @param   memberId
214
    *          Id of the departed server.
215
    * @param   how
216
    *          Description of the departure.
217
    */
218
   private static void missedNotices(String memberId, String how)
219
   {
220
      try
221
      {
222
         Thread.sleep(MembershipLease.GRACE_MS);
223
      }
224
      catch (InterruptedException exc)
225
      {
226
         return;
227
      }
228

  
229
      if (!OwnerLockManager.cacheClosing())
230
      {
231
         com.goldencode.p2j.distributed.cache.CommitNotice.missed("server " + memberId + " " + how);
232
      }
196 233
   }
197 234

  
198 235
   /**
199
-- a/src/com/goldencode/p2j/distributed/lock/OwnerLockFunction.java
236
++ b/src/com/goldencode/p2j/distributed/lock/OwnerLockFunction.java
......
34 34
**                  request or an older one (a late give-up never takes a newer lock); a CANCEL makes the
35 35
**                  older requests stale (LockQueue.giveUp); a call of a request older than the session's
36 36
**                  lock is not applied (olderThanLock()); give-ups with an id skip the call-number test.
37
**     CA  20261029 The locks of a departed member are removed only after the grace of its membership lease
38
**                  (LiveMembers.lockerChecker() in purgeHere() and decide()).
37 39
*/
38 40

  
39 41
/*
......
1127 1129
    */
1128 1130
   private static boolean purgeHere(Region<Object, OwnerLockEntry> region, Object key)
1129 1131
   {
1130
      Predicate<String> live = LiveMembers.checker();
1132
      // a departed member's locks stand until its membership lease expired (it may still be committing)
1133
      Predicate<String> live = LiveMembers.lockerChecker();
1131 1134
      boolean changed = false;
1132 1135
      List<SessionToken> front = Collections.emptyList();
1133 1136
      Object qk = new QueueKey(region.getName(), key);
......
1219 1222
      }
1220 1223

  
1221 1224
      LockType requested = LockType.fromInt(req.type).toWaitVariant();
1222
      Predicate<String> live = null;
1223 1225
      OwnerLockEntry base = before;
1224 1226
      boolean departed = false;
1225 1227

  
1226 1228
      LockPolicy.Outcome outcome = decide(base, token, requested);
1227 1229
      if (outcome == LockPolicy.Outcome.WAIT)
1228 1230
      {
1229
         // remove locks left by departed members so they never block anyone
1230
         live = LiveMembers.checker();
1231
         if (base.hasLockerOutside(live))
1231
         // remove locks left by departed members so they never block anyone, once their membership lease
1232
         // expired (until then they may still be committing under them)
1233
         Predicate<String> lockers = LiveMembers.lockerChecker();
1234
         if (base.hasLockerOutside(lockers))
1232 1235
         {
1233
            base = base.purgeMembers(live);
1236
            base = base.purgeMembers(lockers);
1234 1237
            outcome = decide(base, token, requested);
1235 1238
            LOG.log(Level.WARNING, "[" + req.database + "] lock on " + req.key +
1236 1239
                                   " was held by a departed member; removed");
......
1241 1244
      if (outcome == LockPolicy.Outcome.GRANT && req.update)
1242 1245
      {
1243 1246
         // a request arriving after its member left must not leave a lock behind
1244
         boolean requesterLive = LiveMembers.isLocal(req.member) ||
1245
                                 (live != null ? live : LiveMembers.checker()).test(req.member);
1247
         boolean requesterLive = LiveMembers.isLocal(req.member) || LiveMembers.isLive(req.member);
1246 1248
         if (requesterLive)
1247 1249
         {
1248 1250
            after = base.grant(token, req.member, requested, req.req);
1249
-- a/src/com/goldencode/p2j/distributed/lock/OwnerLockManager.java
1251
++ b/src/com/goldencode/p2j/distributed/lock/OwnerLockManager.java
......
67 67
**                  holding lost locks; lost locks are released locally; added holdsStaleLocks(); the
68 68
**                  re-initialisation participant rebinds the lock regions of the databases still in use
69 69
**                  (reinitialiseAll(), rebind(); not those of deleted tenant databases).
70
**     CA  20261029 purgeDepartedMember() waits for the grace of the departed member's membership lease first;
71
**                  heldKeys() names the current context's locks for the commit's per-owner lease; owner calls
72
**                  note the member which answered (MembershipLease.collector()).
70 73
*/
71 74

  
72 75
/*
......
128 131
import java.util.*;
129 132
import java.util.concurrent.*;
130 133
import java.util.concurrent.atomic.*;
134
import java.util.function.BiConsumer;
135
import java.util.function.LongConsumer;
131 136
import java.util.logging.*;
132 137

  
133 138
import org.apache.geode.CancelException;
......
958 963
   }
959 964

  
960 965
   /**
961
    * Remove the locks of departed members from the local primary entries of every lock region; repeated
962
    * after a few seconds to catch primaries moved here meanwhile. Called by {@link LockMembershipHandler}.
966
    * Remove the locks of departed members from the local primary entries of every lock region, once the
967
    * grace of the departed member's membership lease ended ({@link MembershipLease#GRACE_MS}: it may still be
968
    * committing under them until then); repeated after a few seconds to catch primaries moved here meanwhile.
969
    * Called by {@link LockMembershipHandler}.
963 970
    *
964 971
    * @param   departed
965 972
    *          Id of the departed member, for the log.
......
971 978
         return;
972 979
      }
973 980

  
974
      purgeLocalPrimaries(departed);
975 981
      try
976 982
      {
983
         // the departed member may still be committing under its locks until its membership lease expired
984
         Thread.sleep(MembershipLease.GRACE_MS);
985
         purgeLocalPrimaries(departed);
977 986
         Thread.sleep(PURGE_AGAIN_MS);
978 987
      }
979 988
      catch (InterruptedException exc)
......
1029 1038
   }
1030 1039

  
1031 1040
   /**
1041
    * Name the record and table locks the current context holds, in every lock region of this server, for
1042
    * the commit's per-owner membership lease ({@code MembershipLease.awaitCommit}).
1043
    *
1044
    * @param   consumer
1045
    *          Takes the lock region and the key of each lock.
1046
    * @param   recordIds
1047
    *          Takes the record id of each record lock (not of table locks).
1048
    */
1049
   public static void heldKeys(BiConsumer<Region<?, ?>, Object> consumer, LongConsumer recordIds)
1050
   {
1051
      for (OwnerLockManager<?> manager : MANAGERS.values())
1052
      {
1053
         Context<?> local = manager.context.get(false);
1054
         Region<OwnerLockKey, OwnerLockEntry> region = manager.lockTable;
1055
         if (local == null || region == null || local.locked.isEmpty())
1056
         {
1057
            continue;
1058
         }
1059

  
1060
         for (Map<? extends RecordIdentifier<?>, LockType> locks : local.locked.values())
1061
         {
1062
            for (RecordIdentifier<?> ident : locks.keySet())
1063
            {
1064
               consumer.accept(region, new OwnerLockKey(ident));
1065
               Long id = ident.getRecordID();
1066
               if (id != null)
1067
               {
1068
                  recordIds.accept(id);
1069
               }
1070
            }
1071
         }
1072
      }
1073
   }
1074

  
1075
   /**
1032 1076
    * Re-initialisation participant ({@link ClusterGate#ORDER_LOCKS}): on Geode's reconnected cache, register
1033 1077
    * the membership listener again and bind every lock manager of {@link #MANAGERS} to its lock region again
1034 1078
    * (the regions of the closed cache are dropped from {@link #REGIONS}; their leases are replaced by
......
2926 2970
         boolean fence = request.fence || attempt > 1;
2927 2971
         OwnerLockFunction.Request sent = request.sent(CALL_SEQ.incrementAndGet(), fence);
2928 2972
         long start = System.nanoTime();
2973
         // the collector notes the owner which answers (its answer renews its membership lease)
2929 2974
         ResultCollector<Object, List<Object>> rc = (ResultCollector<Object, List<Object>>)
2930 2975
            FunctionService.onRegion(region)
2931 2976
                           .withFilter(Collections.singleton(request.key))
2932 2977
                           .setArguments(sent)
2978
                           .withCollector(MembershipLease.collector())
2933 2979
                           .execute(OwnerLockFunction.ID);
2934 2980
         try
2935 2981
         {
2936
-- a/src/com/goldencode/p2j/distributed/orm/OwnerUniqueFunction.java
2982
++ b/src/com/goldencode/p2j/distributed/orm/OwnerUniqueFunction.java
......
8 8
** 001 ES  20261009 Initial version (#4369 owner-side unique tracker).
9 9
**     ES  20261026 Liveness checks use LiveMembers (cached view), checked in purge() under the key's lock.
10 10
**     ES  20261026 Removed an unused import.
11
**     CA  20261029 A departed member's reservation is taken over or purged only after the grace of its
12
**                  membership lease (LiveMembers.holdsLocks()).
11 13
*/
12 14

  
13 15
/*
......
300 302
         return new Answer(key, Outcome.MINE, null);
301 303
      }
302 304

  
303
      if (!LiveMembers.isLive(stored.getMember()))
305
      if (!LiveMembers.holdsLocks(stored.getMember()))
304 306
      {
305
         // a departed member's reservation never blocks anyone
307
         // a departed member's reservation never blocks anyone once its membership lease expired
306 308
         if (!region.replace(key, stored, holder))
307 309
         {
308 310
            return null;
......
324 326
   }
325 327

  
326 328
   /**
327
    * Remove the reservation of a key if it belongs to a member which left the cluster.
329
    * Remove the reservation of a key if it belongs to a member which left the cluster (after the grace of its
330
    * membership lease, {@link LiveMembers#holdsLocks}).
328 331
    *
329 332
    * @param   region
330 333
    *          The reservation region.
......
341 344
      {
342 345
         UniqueReservation stored = region.get(key);
343 346

  
344
         return stored != null && !LiveMembers.isLive(stored.getMember()) && region.remove(key, stored);
347
         return stored != null && !LiveMembers.holdsLocks(stored.getMember()) && region.remove(key, stored);
345 348
      }
346 349
      finally
347 350
      {
348
-- a/src/com/goldencode/p2j/distributed/orm/OwnerUniqueIndex.java
351
++ b/src/com/goldencode/p2j/distributed/orm/OwnerUniqueIndex.java
......
30 30
**                  again in a new epoch of the gate (re-initialisation participant); pending keys and keys
31 31
**                  carry the epoch they were made in: a reservation of an older epoch is never released or
32 32
**                  restored, and its state never replaces the state of the new epoch.
33
**     CA  20261029 purgeDepartedMember() waits for the grace of the departed member's membership lease first;
34
**                  pendingKeys() names the session's pending keys for the commit's per-owner lease; owner
35
**                  calls note the member which answered (MembershipLease.collector()).
33 36
*/
34 37

  
35 38
/*
......
88 91
package com.goldencode.p2j.distributed.orm;
89 92

  
90 93

  
94
import java.lang.ref.*;
91 95
import java.util.*;
92 96
import java.util.concurrent.*;
93 97
import java.util.concurrent.locks.*;
98
import java.util.function.BiConsumer;
94 99
import java.util.logging.*;
95 100

  
96 101
import org.apache.geode.CancelException;
......
171 176
   /** End-of-transaction removals of the current session, not sent yet. */
172 177
   private static final ThreadLocal<List<Release>> REMOVED = ThreadLocal.withInitial(ArrayList::new);
173 178

  
179
   /** Every index of this server (weakly held, by identity), for {@link #pendingKeys} (guarded by itself). */
180
   private static final List<WeakReference<OwnerUniqueIndex>> ALL = new ArrayList<>();
181

  
174 182
   /** Reservation regions by database name (lower case). */
175 183
   private static final Map<String, Region<ReservedKey, UniqueReservation>> REGIONS =
176 184
      new ConcurrentHashMap<>();
......
233 241
      super(fieldMap, NO_LOCK, props);
234 242
      this.uniqueId = uniqueId;
235 243
      this.dbName = dbName;
244

  
245
      synchronized (ALL)
246
      {
247
         ALL.removeIf(r -> r.get() == null);
248
         ALL.add(new WeakReference<>(this));
249
      }
250
   }
251

  
252
   /**
253
    * Name the unique keys pending in open transactions, of the current epoch,
254
    * in every reservation region, of the given records, for the commit's per-owner membership lease
255
    * ({@code MembershipLease.awaitCommit}): the records the current session holds locked (a session changes
256
    * or creates a record only under its lock), so the keys of other sessions' records are left out (a
257
    * record id of another table may add a key, which only adds a member to wait for).
258
    *
259
    * @param   recordIds
260
    *          The record ids.
261
    * @param   consumer
262
    *          Takes the reservation region and the reserved key.
263
    */
264
   public static void pendingKeys(Set<Long> recordIds, BiConsumer<Region<?, ?>, Object> consumer)
265
   {
266
      if (recordIds.isEmpty())
267
      {
268
         return;
269
      }
270

  
271
      List<OwnerUniqueIndex> indexes = new ArrayList<>();
272
      synchronized (ALL)
273
      {
274
         for (WeakReference<OwnerUniqueIndex> r : ALL)
275
         {
276
            OwnerUniqueIndex index = r.get();
277
            if (index != null)
278
            {
279
               indexes.add(index);
280
            }
281
         }
282
      }
283

  
284
      long epoch = ClusterGate.epoch();
285
      for (OwnerUniqueIndex index : indexes)
286
      {
287
         Region<ReservedKey, UniqueReservation> r = index.region;
288
         if (r == null || index.boundEpoch != epoch || index.pending.isEmpty())
289
         {
290
            continue;
291
         }
292

  
293
         for (Long id : recordIds)
294
         {
295
            Pending p = index.pending.get(id);
296
            if (p != null && p.epoch == epoch && !p.reserved.hasNulls())
297
            {
298
               consumer.accept(r, p.reserved);
299
            }
300
         }
301
      }
236 302
   }
237 303

  
238 304
   /**
......
422 488
                                                         Set<ReservedKey>                       filter,
423 489
                                                         OwnerUniqueFunction.Request            request)
424 490
   {
491
      // the collector notes the owners which answer (their answers renew their membership lease)
425 492
      ResultCollector<Object, List<Object>> rc = (ResultCollector<Object, List<Object>>)
426 493
         FunctionService.onRegion(region)
427 494
                        .withFilter(filter)
428 495
                        .setArguments(request)
496
                        .withCollector(MembershipLease.collector())
429 497
                        .execute(OwnerUniqueFunction.ID);
430 498

  
431 499
      Map<ReservedKey, OwnerUniqueFunction.Answer> answers = new HashMap<>();
......
1298 1366
   }
1299 1367

  
1300 1368
   /**
1301
    * Remove a departed member's reservations from the keys this member owns, now and again a little
1302
    * later (key ownership may still be moving).
1369
    * Remove a departed member's reservations from the keys this member owns, once the grace of its
1370
    * membership lease ended ({@link MembershipLease#GRACE_MS}), and again a little later (key ownership may
1371
    * still be moving).
1303 1372
    *
1304 1373
    * @param   departed
1305 1374
    *          Id of the departed member.
......
1311 1380
         return;
1312 1381
      }
1313 1382

  
1314
      purgeLocalPrimaries(departed);
1315 1383
      try
1316 1384
      {
1385
         // the departed member may still be committing under its locks until its membership lease expired
1386
         Thread.sleep(MembershipLease.GRACE_MS);
1387
         purgeLocalPrimaries(departed);
1317 1388
         Thread.sleep(PURGE_AGAIN_MS);
1318 1389
      }
1319 1390
      catch (InterruptedException exc)
1320
-- a/src/com/goldencode/p2j/persist/FastFindCache.java
1391
++ b/src/com/goldencode/p2j/persist/FastFindCache.java
......
61 61
**     CA  20261029 Added getPermanentInstance(Type, String): the lookup by database type and name (the meta
62 62
**                  database has the primary's name), used by the cluster commit notice; added
63 63
**                  invalidateAllPermanent() (notices missed while this server was not a cluster member).
64
**     CA  20261029 Removed the per-change invalidation broadcast to the other servers (broadcast(), the
65
**                  name-only getPermanentInstance(String), setIsFromRemoteRequest()): the cluster commit
66
**                  notice is the only invalidation across the servers.
64 67
*/
65 68

  
66 69
/*
......
119 122
package com.goldencode.p2j.persist;
120 123

  
121 124
import com.goldencode.cache.*;
122
import com.goldencode.p2j.distributed.cache.*;
123
import com.goldencode.p2j.distributed.config.*;
124 125
import com.goldencode.cache.Cache.Entry;
125 126
import com.goldencode.p2j.persist.orm.*;
126 127
import com.goldencode.p2j.security.*;
......
128 129
import com.goldencode.p2j.util.logging.*;
129 130

  
130 131
import com.goldencode.util.ResizableHashMap;
131
import org.apache.geode.cache.execute.*;
132
import org.apache.geode.distributed.*;
133 132

  
134 133
import java.lang.ref.WeakReference;
135 134
import java.util.*;
......
212 211
   /** Differentiate between permanent and temporary tables and caches. */
213 212
   private final boolean isPermanent;
214 213
   
215
   /** Flag indicating distributed mode */
216
   private final boolean distributedMode;
217
   
218 214
   /**
219 215
    * Flag indicating this instance is switched off: it is for a permanent database and the directory
220 216
    * node {@code persistence/fast-find-cache} is {@code false}. A disabled instance never caches, never
221
    * invalidates and never broadcasts.
217
    * invalidates.
222 218
    */
223 219
   private final boolean disabled;
224 220
   
......
230 226
   private final AtomicLong generation = new AtomicLong();
231 227
   
232 228
   /**
233
    * Thread-local flag to mark threads that are handling remote invalidation requests.
234
    */
235
   private static final ThreadLocal<Boolean> isFromRemoteRequest = ThreadLocal.withInitial(() -> false);
236
   
237
   /**
238 229
    * Constructor for internal use.
239 230
    *
240 231
    * @param   isPermanent
......
243 234
   private FastFindCache(boolean isPermanent, Database database)
244 235
   {
245 236
      this.isPermanent = isPermanent;
246
      this.distributedMode = ClusterConfig.isEnabled();
247 237
      this.database  = database;
248 238
      this.disabled  = isPermanent && !permanentEnabled;
249 239
      
......
258 248
    * for it. For the permanent databases, the cache can be switched off with the directory boolean node
259 249
    * {@code persistence/fast-find-cache} (default {@code true}, read once at class load). The instance
260 250
    * returned then is inert: {@code get} always misses, {@code put} and all {@code invalidate} variants
261
    * do nothing (and nothing is broadcast to other servers), and no state is accumulated. The temp-table
262
    * instances are not affected by this setting.
251
    * do nothing, and no state is accumulated. The temp-table instances are not affected by this setting.
263 252
    * 
264 253
    * @param   temporary
265 254
    *          Use {@code true} to get the instance object for a temp-table database. They are context-local
......
307 296
   }
308 297

  
309 298
   /**
310
    * Get the existing instance of a permanent database by the database's name, without a user context
311
    * (for the cluster invalidation functions, which run on Geode threads).
312
    *
313
    * @param   dbName
314
    *          The name of the database, as {@link Database#getName()} returns it.
315
    *
316
    * @return  The instance, or {@code null} if this server has none (it then caches nothing for that
317
    *          database).
318
    */
319
   public static FastFindCache getPermanentInstance(String dbName)
320
   {
321
      for (Map.Entry<Database, FastFindCache> e : permanentInstances.entrySet())
322
      {
323
         if (e.getKey().getName().equals(dbName))
324
         {
325
            return e.getValue();
326
         }
327
      }
328

  
329
      return null;
330
   }
331

  
332
   /**
333 299
    * Get the existing instance of a permanent database by the database's type and name, without a user
334 300
    * context (for the cluster commit notice, which runs on Geode threads). The type tells apart a primary
335 301
    * database and the meta database, which have the same name.
......
488 454
   }
489 455
   
490 456
   /**
491
    * Marks the current thread as a remote-request handler. Once set, {@code broadcast()} becomes
492
    * a no-op for this thread, preventing infinite re-distribution of invalidation messages.
493
    */
494
   public static void setIsFromRemoteRequest()
495
   {
496
      isFromRemoteRequest.set(true);
497
   }
498
   
499
   /**
500 457
    * Query the cache for a value.
501 458
    *
502 459
    * @param   k
......
746 703
                        });
747 704
         }
748 705
      }
749
      
750
      if (distributedMode && isPermanent)
751
      {
752
         broadcast(new FastFindCacheInvalidationRequest.InvalidateAllIndicesForTable(database.getName(),
753
                                                                                     dmoUid));
754
      }
755 706
   }
756 707
   
757 708
   /**
......
928 879
            }
929 880
         }
930 881
      }
931
      
932
      if (distributedMode && isPermanent)
933
      {
934
         broadcast(new FastFindCacheInvalidationRequest.InvalidateIndices(
935
            database.getName(), dmoUid, unique, nonUnique));
936
         
937
      }
938 882
   }
939 883

  
940 884
   /**
......
957 901
         {
958 902
            l3.clear();
959 903
         }
960
         
961
         if (distributedMode && isPermanent)
962
         {
963
            broadcast(new FastFindCacheInvalidationRequest.InvalidateIndex(
964
               database.getName(), dmoUid, idxUid));
965
         }
966 904
      }
967 905
   }
968 906

  
......
982 920
            map.forEach((idxId, cache) -> cache.clear());
983 921
         }
984 922
      }
985
      
986
      if (distributedMode && isPermanent)
987
      {
988
         broadcast(new FastFindCacheInvalidationRequest.InvalidateDmoList(
989
            database.getName(), dmoList));
990
      }
991 923
   }
992 924
   
993 925
   /**
994
    * Broadcasts a cache invalidation request to all other FWD server members in the distributed
995
    * system. Non-server members (e.g. Geode locators) are excluded, since they can neither
996
    * deserialize nor execute FWD functions.
997
    * <p>
998
    * To prevent cyclic broadcasting (echoes) across the cluster, this method immediately
999
    * returns if the current execution context was triggered by a remote request.
1000
    * If there are other active FWD servers in the Geode cluster, it uses the
1001
    * {@code FunctionService} to execute the {@code FastFindCacheInvalidationFunction}
1002
    * on those peers, passing the invalidation payload.
1003
    * </p>
1004
    *
1005
    * @param request
1006
    *        The payload containing the specific cache invalidation details to broadcast.
1007
    */
1008
   private void broadcast(FastFindCacheInvalidationRequest request)
1009
   {
1010
      if (isFromRemoteRequest.get())
1011
      {
1012
         return;
1013
      }
1014

  
1015
      if (com.goldencode.p2j.distributed.cache.CommitNotice.isEnabled())
1016
      {
1017
         // the other servers learn of committed changes from the commit notice; these invalidations are
1018
         // mostly sent before the commit (useless there: the change is not visible yet) and name the table
1019
         // by this JVM's DMO uid, which is another table, or none, on another server
1020
         return;
1021
      }
1022

  
1023
      Set<DistributedMember> otherMembers = GeodeConfig.getOtherServerMembers();
1024

  
1025
      if (otherMembers.isEmpty())
1026
      {
1027
         return;
1028
      }
1029

  
1030
      FunctionService.onMembers(otherMembers)
1031
                     .setArguments(request)
1032
                     .execute(FastFindCacheInvalidationFunction.class.getName());
1033
   }
1034
   
... This diff was truncated because it exceeds the maximum size that can be displayed.