docs: private CompletableFuture<Void> sendHeartbeat(GossipMember member) {
      return bootstrapService.getMessagingService().sendAndReceive(member.address(), HEARTBEAT_MESSAGE, SERIALIZER.encode(localMember))
          .whenCompleteAsync((response, error) -> {
            if (error == null) {
              Collection<GossipMember> remoteMembers = SERIALIZER.decode(response);
              for (GossipMember remoteMember : remoteMembers) {
                if (!remoteMember.id().equals(localMember.id())) {
                  updateMember(remoteMember, remoteMember.id().equals(member.id()));
                }
              }
            } else {
              LOGGER.debug("{} - Sending heartbeat to {} failed", localMember.id(), member, error);
              if (member.isReachable()) {
                member.setReachable(false);
                post(new GroupMembershipEvent(GroupMembershipEvent.Type.REACHABILITY_CHANGED, member));
              }
  
              PhiAccrualFailureDetector failureDetector = failureDetectors.computeIfAbsent(member.id(), n -> new PhiAccrualFailureDetector());
              double phi = failureDetector.phi();
              if (phi >= config.getPhiFailureThreshold()
                  || (phi == 0.0 && System.currentTimeMillis() - failureDetector.lastUpdated() > config.getFailureTimeout().toMillis())) {
                if (members.remove(member.id()) != null) {
                  failureDetectors.remove(member.id());
                  post(new GroupMembershipEvent(GroupMembershipEvent.Type.MEMBER_REMOVED, member));
                }
              }
            }
          }, heartbeatScheduler).exceptionally(e -> null)
          .thenApply(v -> null);
    }