
net.e6tech.elements.web.federation.Beacon Maven / Gradle / Ivy
package net.e6tech.elements.web.federation;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import net.e6tech.elements.common.federation.Member;
import net.e6tech.elements.common.logging.Logger;
import net.e6tech.elements.common.resources.Provision;
import net.e6tech.elements.common.subscribe.Notice;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
public class Beacon {
public static Logger logger = Logger.getLogger();
private CollectiveImpl collective;
private Cache events;
private final Random random = new Random();
private volatile boolean shutdown = false;
private ReentrantLock seeding = new ReentrantLock();
private List seedFrequencies = Collections.emptyList();
private Thread eventThread;
private Map frequencies = new ConcurrentHashMap<>(128); // memberId to frequency
public CollectiveImpl getCollective() {
return collective;
}
public void setCollective(CollectiveImpl collective) {
this.collective = collective;
}
protected void seeds(String[] seeds) {
if (seeds == null) {
seedFrequencies = Collections.emptyList();
return;
}
List list = new ArrayList<>(seeds.length);
for (String s : seeds) {
list.add(new HailingFrequency(s, collective));
}
seedFrequencies = Collections.unmodifiableList(list);
}
public boolean isShutdown() {
return shutdown;
}
public void setShutdown(boolean shutdown) {
this.shutdown = shutdown;
}
public Collection members() {
return frequencies.values().stream().map(HailingFrequency::getMember).collect(Collectors.toList());
}
protected int knownFrequencies() {
return frequencies.size();
}
protected void trimFrequencies() {
frequencies.values().removeIf(f -> !collective.getHostedMembers().containsKey(f.memberId()) &&
f.getMember().getExpiration() < System.currentTimeMillis());
}
public HailingFrequency getFrequency(String memberId) {
return frequencies.get(memberId);
}
protected HailingFrequency updateFrequency(Member member) {
HailingFrequency f = frequencies.get(member.getMemberId());
if (f == null) {
f = new HailingFrequency(member, collective);
frequencies.put(member.getMemberId(), f);
if (f.getMember().getExpiration() < member.getExpiration())
f.setMember(member);
HailingFrequency f2 = f;
collective.getExecutor().execute(() -> collective.getListeners().forEach(listener -> listener.added(f2)));
} else if (f.getMember().getExpiration() < member.getExpiration()) {
f.setMember(member);
}
return f;
}
protected void updateFrequencies(Collection list) {
for (Member member : list) {
updateFrequency(member);
}
}
protected void removeFrequency(HailingFrequency frequency) {
if (frequency != null && !collective.getHostedMembers().containsKey(frequency.getMember().getMemberId())) {
HailingFrequency c = frequencies.remove(frequency.memberId());
collective.getExecutor().execute(() -> collective.getListeners().forEach(listener -> listener.removed(c)));
}
}
protected void removeFrequencies(Collection list) {
for (Member member : list) {
if (!collective.getHostedMembers().containsKey(member.getMemberId())) {
HailingFrequency c = frequencies.remove(member.getMemberId());
collective.getExecutor().execute(() -> collective.getListeners().forEach(listener -> listener.removed(c)));
}
}
}
protected Map frequencies() {
Map map = new HashMap<>(frequencies);
return Collections.unmodifiableMap(map);
}
// need to have a background thread to poll liveliness.
public void start() {
if (events != null) {
events.invalidateAll();
events.cleanUp();
}
events = CacheBuilder.newBuilder()
.concurrencyLevel(Provision.cacheBuilderConcurrencyLevel)
.initialCapacity(collective.getEventCacheInitialCapacity())
.expireAfterAccess(collective.getEventCacheExpire(), TimeUnit.MILLISECONDS)
.build();
shutdown = false;
collective.getHostedMembers().values().forEach(m -> {
collective.refresh(m);
updateFrequency(m);
});
Thread thread = new Thread(this::run);
thread.start();
}
private void run() {
// wait till the api is up
while (frequencies.size() == 0) {
HailingFrequency frequency = frequencies.values().iterator().next();
try {
frequency.beacon().members();
break;
} catch (Exception e) {
// ignore
}
try {
Thread.sleep(100L); // wait for its own API to start.
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
// ignore
}
}
// send out new Event to seeds
logger.trace("{} start seeding.", collective.getHostAddress());
seeds("starting");
logger.trace("{} done seeding.", collective.getHostAddress());
// create a thread to process events
eventThread = new Thread(this::events);
eventThread.start();
// create a thread to sync members
Thread sync = new Thread(this::sync);
sync.start();
// create a thread to send out new lease events
try {
Thread.sleep(collective.getRenewalInterval());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Thread renewal = new Thread(this::renewal);
renewal.start();
}
private void seeds(String from) {
if (!seeding.tryLock())
return;
try {
while (true) {
if (seedFrequencies.isEmpty()) {
announce();
break;
} else if (syncMembers(seedFrequencies)) { // may the seeds are dead? If not, announce.
logger.trace("{} {} seeds announce ", collective.getHostAddress(), from);
announce();
break;
}
try {
Thread.sleep(collective.getSeedRefreshInterval());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} finally {
seeding.unlock();
}
}
public void onEvent(Event event) {
if (shutdown)
return;
// seen it before?
synchronized (events) {
Event existing = events.getIfPresent(event.getUuid());
if (existing != null) {
if (existing != null && existing.getUuid().equals(event.getUuid())) {
event.addVisited(existing.getVisited());
if (event.getCycle() > existing.getCycle())
event.setCycle(existing.getCycle());
events.put(event.getUuid(), event);
return;
}
}
}
// updating frequencies.
logger.trace("{} onEvent: {}" , collective.getHostAddress(), event);
if (Objects.equals(event.getDomainName(), collective.getDomainName()) && event.getCollectiveType() == collective.getType()) {
if (event.getType() == Event.Type.ANNOUNCE) {
updateFrequencies(event.getMembers());
} else if (event.getType() == Event.Type.REMOVE) {
removeFrequencies(event.getMembers());
} else if (event.getType() == Event.Type.BROADCAST) {
Notice notice = collective.getSubZero().thaw(event.getPayload());
if (notice != null)
collective.publishInternal(notice);
}
}
// add to events so that it can be propagated.
if (event.getCycle() > 0) {
synchronized (events) {
events.put(event.getUuid(), event);
// had to create hostedMemberIds then call event.addVisited to avoid current modification
Set hostedMemberIds = new HashSet<>(collective.getHostedMembers().size() * 2 + 1);
collective.getHostedMembers().values().forEach(m -> hostedMemberIds.add(m.getMemberId()));
event.addVisited(hostedMemberIds);
event.getVisited().addAll(collective.getHostedMembers().keySet());
events.notifyAll();
}
}
}
private void decrementCycle(Event event) {
if (event.getCycle() > 0) {
event.setCycle(event.getCycle() - 1);
}
}
private void announce() {
trimFrequencies();
collective.getHostedMembers().values().forEach(collective::refresh);
List list = new ArrayList<>(collective.getHostedMembers().size());
list.addAll(collective.getHostedMembers().values());
Event event = new Event(collective.getDomainName(), Event.Type.ANNOUNCE, collective.getType(), list, collective.getCycle());
event.getVisited().addAll(collective.getHostedMembers().keySet());
events.put(event.getUuid(), event);
collective.onAnnounced(event);
gossip(event);
}
void announce(Member member) {
collective.refresh(member);
List list = new ArrayList<>();
list.add(member);
onEvent(new Event(collective.getDomainName(), Event.Type.ANNOUNCE, collective.getType(), list, collective.getCycle()));
}
void broadcast(Notice notice) {
trimFrequencies();
collective.getHostedMembers().values().forEach(collective::refresh);
List list = new ArrayList<>(collective.getHostedMembers().size());
list.addAll(collective.getHostedMembers().values());
Event event = new Event(collective.getDomainName(), Event.Type.BROADCAST, collective.getType(), list, collective.getCycle());
event.getVisited().addAll(collective.getHostedMembers().keySet());
events.put(event.getUuid(), event);
event.setPayload(collective.getSubZero().freeze(notice));
gossip(event);
}
private void renewal() {
while (!shutdown) {
try {
long interval = collective.getRenewalInterval() + collective.getCycle() * collective.getEventInterval();
if (knownFrequencies() <= collective.getHostedMembers().size()) {
seeds("renewal");
Thread.sleep(interval);
} else {
long now = System.currentTimeMillis();
logger.trace("{} renewal: ", collective.getHostAddress());
announce();
long sleep = interval - (System.currentTimeMillis() - now);
if (sleep < 0) // during trace
sleep = collective.getRenewalInterval();
Thread.sleep(sleep);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception ex) {
logger.error("renewal", ex);
}
}
}
private void events() {
while (!shutdown) {
try {
processEvents();
Thread.sleep(collective.getEventInterval());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} catch (Exception ex) {
logger.error("events", ex);
}
}
}
private void processEvents() {
List copy = new LinkedList<>();
int before;
int after;
synchronized (events) {
while (events.size() == 0 || knownFrequencies() <= collective.getHostedMembers().size()) {
try {
events.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
Map map = events.asMap();
copy.addAll(map.values());
copy.forEach(this::decrementCycle);
before = copy.size();
map.entrySet().removeIf(e -> e.getValue().getCycle() <= 0);
after = copy.size();
}
logger.trace("{} event size {}, {}", collective.getHostAddress(), before, after);
copy.forEach(this::gossip);
}
private void sync() {
while (!shutdown) {
try {
if (knownFrequencies() >= collective.getHostedMembers().size()) {
syncMembers(frequencies().values());
}
Thread.sleep(collective.getSyncInterval());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception ex) {
logger.error("sync", ex);
}
}
}
private boolean syncMembers(Collection fList) {
if (fList.isEmpty())
return false;
// filter out self
List list = fList.stream().filter(c -> !collective.getHostedMembers().containsKey(c.memberId())).collect(Collectors.toList());
long interval = collective.getRenewalInterval() + collective.getCycle() * collective.getEventInterval();
list = list.stream().filter(c -> {
boolean filter = System.currentTimeMillis() - c.getLastConnectionError() > c.getConsecutiveError() * interval;
if (System.currentTimeMillis() - c.getLastConnectionError() > collective.getDeadMemberRenewalInterval())
return true;
return filter;
})
.collect(Collectors.toList());
if (list.isEmpty())
return true; // because list contain only self
URL hostAddress = null;
try {
hostAddress = new URL(collective.getHostAddress());
} catch (MalformedURLException e) {
logger.warn("Invalid host address " + collective.getHostAddress());
return false;
}
while (true) {
HailingFrequency frequency = null;
try {
if (list.isEmpty())
return false;
int n = random.nextInt(list.size());
frequency = list.remove(n);
URL memberURL = new URL(frequency.getMember().getAddress());
if (! hostAddress.equals(memberURL)) {
Collection list2 = frequency.beacon().members();
logger.trace("{} syncMembers: {}", collective.getHostAddress(), list2);
updateFrequencies(list2);
return true;
}
} catch (Exception ex) {
removeFrequency(frequency);
}
}
}
public void shutdown() {
if (shutdown)
return;
shutdown = true;
long now = System.currentTimeMillis();
collective.getHostedMembers().values().forEach(m -> m.setExpiration(now));
List list = new ArrayList<>(collective.getHostedMembers().size());
list.addAll(collective.getHostedMembers().values());
Event event = new Event(collective.getDomainName(), Event.Type.REMOVE, collective.getType(), list, collective.getCycle());
event.getVisited().addAll(collective.getHostedMembers().keySet());
for (Member member : list) {
HailingFrequency c = frequencies.remove(member.getMemberId());
collective.getExecutor().execute(() -> collective.getListeners().forEach(listener -> listener.removed(c)));
}
gossip(event);
if (eventThread != null) {
eventThread.interrupt();
eventThread = null;
}
if (events != null) {
events.invalidateAll();
}
}
/**
* Never call gossip outside of announce , processEvent and shutdown. Otherwise, too many synchronous api calls.
* @param event
* @return
*/
private void gossip(Event event) {
// filter out already visited
List list = frequencies.values().stream()
.filter(c -> !event.getVisited().contains(c.memberId()))
.collect(Collectors.toList());
if (list.isEmpty()) {
return;
}
List chosen;
int fanout = collective.getFanout();
if (fanout >= list.size()) {
chosen = list;
} else {
chosen = new ArrayList<>(fanout);
for (int i = 0; i < fanout; i++) {
int n = random.nextInt(list.size());
chosen.add(list.get(n));
list.remove(n);
}
}
collective.getExecutor().execute(() -> {
chosen.forEach(frequency -> {
logger.trace("{} gossip to {} {}", collective.getHostAddress(), frequency.memberId(), event);
try {
frequency.beacon().onEvent(event);
} catch (Exception ex) {
// remove connector from members.
removeFrequency(frequency);
}
});
});
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy