package org.jgroups.protocols; import org.jgroups.Address; import org.jgroups.Event; import org.jgroups.PhysicalAddress; import org.jgroups.View; import org.jgroups.annotations.ManagedAttribute; import org.jgroups.annotations.ManagedOperation; import org.jgroups.annotations.Property; import org.jgroups.util.NameCache; import org.jgroups.util.Responses; import org.jgroups.util.TimeScheduler; import org.jgroups.util.Util; import java.io.*; import java.util.*; import java.util.concurrent.Future; import java.util.regex.Pattern; /** * Simple discovery protocol which uses a file on shared storage such as an SMB share, NFS mount or S3. The local * address information, e.g. UUID and physical addresses mappings are written to the file and the content is read and * added to our transport's UUID-PhysicalAddress cache.<p/> * The design is at doc/design/FILE_PING.txt * @author Bela Ban */ public class FILE_PING extends Discovery { protected static final String SUFFIX=".list"; protected static final Pattern regexp=Pattern.compile("[\0<>:\"/\\|?*]"); /* ----------------------------------------- Properties -------------------------------------------------- */ @Property(description="The absolute path of the shared file") protected String location=File.separator + "tmp" + File.separator + "jgroups"; @Property(description="If true, on a view change, the new coordinator removes files from old coordinators") protected boolean remove_old_coords_on_view_change; @Property(description="If true, on a view change, the new coordinator removes all data except its own") protected boolean remove_all_data_on_view_change; @Property(description="The max number of times my own information should be written to the storage after a view change") protected int info_writer_max_writes_after_view=2; @Property(description="Interval (in ms) at which the info writer should kick in") protected long info_writer_sleep_time=10000; @Property(description = "If set, a shutdown hook is registered with the JVM to remove the local address " + "from the store. Default is true", writable = false) protected boolean register_shutdown_hook = true; @ManagedAttribute(description="Number of writes to the file system or cloud store") protected int writes; @ManagedAttribute(description="Number of reads from the file system or cloud store") protected int reads; /* --------------------------------------------- Fields ------------------------------------------------------ */ protected File root_dir=null; protected static final FilenameFilter filter=(dir, name1) -> name1.endsWith(SUFFIX); protected Future<?> info_writer; public boolean isDynamic() {return true;} @ManagedAttribute(description="Whether the InfoWriter task is running") public synchronized boolean isInfoWriterRunning() {return info_writer != null && !info_writer.isDone();} @ManagedOperation(description="Causes the member to write its own information into the DB, replacing an existing entry") public void writeInfo() {if(is_coord) writeAll();} public void init() throws Exception { super.init(); createRootDir(); if(register_shutdown_hook) { Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { remove(cluster_name, local_addr); } }); } } public void stop() { super.stop(); stopInfoWriter(); remove(cluster_name, local_addr); } public void resetStats() { super.resetStats(); reads=writes=0; } public Object down(Event evt) { switch(evt.getType()) { case Event.VIEW_CHANGE: View old_view=view; boolean previous_coord=is_coord; Object retval=super.down(evt); View new_view=evt.getArg(); handleView(new_view, old_view, previous_coord != is_coord); return retval; } return super.down(evt); } public void findMembers(final List<Address> members, final boolean initial_discovery, Responses responses) { try { readAll(members, cluster_name, responses); if(responses.isEmpty()) { PhysicalAddress physical_addr=(PhysicalAddress)down(new Event(Event.GET_PHYSICAL_ADDRESS,local_addr)); PingData coord_data=new PingData(local_addr, true, NameCache.get(local_addr), physical_addr).coord(is_coord); write(Collections.singletonList(coord_data), cluster_name); return; } PhysicalAddress phys_addr=(PhysicalAddress)down_prot.down(new Event(Event.GET_PHYSICAL_ADDRESS, local_addr)); PingData data=responses.findResponseFrom(local_addr); // the logical addr *and* IP address:port have to match if(data != null && data.getPhysicalAddr().equals(phys_addr)) { if(data.isCoord() && initial_discovery) responses.clear(); else ; // use case #1 if we have predefined files: most members join but are not coord } else { sendDiscoveryResponse(local_addr, phys_addr, NameCache.get(local_addr), null, false); } } finally { responses.done(); } } /** Only add the discovery response if the logical address is not present or the physical addrs are different */ protected boolean addDiscoveryResponseToCaches(Address mbr, String logical_name, PhysicalAddress physical_addr) { PhysicalAddress phys_addr=(PhysicalAddress)down_prot.down(new Event(Event.GET_PHYSICAL_ADDRESS, mbr)); boolean added=!Objects.equals(phys_addr, physical_addr); super.addDiscoveryResponseToCaches(mbr, logical_name, physical_addr); if(added && is_coord) writeAll(); return added; } protected static String addressToFilename(Address mbr) { String logical_name=NameCache.get(mbr); String name=(addressAsString(mbr) + (logical_name != null? "." + logical_name + SUFFIX : SUFFIX)); return regexp.matcher(name).replaceAll("-"); } protected void createRootDir() { root_dir=new File(location); if(root_dir.exists()) { if(!root_dir.isDirectory()) throw new IllegalArgumentException("location " + root_dir.getPath() + " is not a directory"); } else root_dir.mkdirs(); if(!root_dir.exists()) throw new IllegalArgumentException("location " + root_dir.getPath() + " could not be accessed"); } // remove all files which are not from the current members protected void handleView(View new_view, View old_view, boolean coord_changed) { if(is_coord) { if(coord_changed) { if(remove_all_data_on_view_change) removeAll(cluster_name); else if(remove_old_coords_on_view_change) { Address old_coord=old_view != null? old_view.getCreator() : null; if(old_coord != null) remove(cluster_name, old_coord); } } if(coord_changed || View.diff(old_view, new_view)[1].length > 0) { writeAll(); if(remove_all_data_on_view_change || remove_old_coords_on_view_change) startInfoWriter(); } } else if(coord_changed) // I'm no longer the coordinator remove(cluster_name, local_addr); } protected void remove(String clustername, Address addr) { if(clustername == null || addr == null) return; File dir=new File(root_dir, clustername); if(!dir.exists()) return; log.debug("remove %s", clustername); String filename=addressToFilename(addr); File file=new File(dir, filename); deleteFile(file); } /** Removes all files for the given cluster name */ protected void removeAll(String clustername) { if(clustername == null) return; File dir=new File(root_dir, clustername); if(!dir.exists()) return; File[] files=dir.listFiles(filter); // finds all files ending with '.list' for(File file: files) file.delete(); } protected void readAll(List<Address> members, String clustername, Responses responses) { File dir=new File(root_dir, clustername); if(!dir.exists()) dir.mkdir(); File[] files=dir.listFiles(filter); // finds all files ending with '.list' for(File file: files) { List<PingData> list=null; // implementing a simple spin lock doing a few attempts to read the file // this is done since the file may be written in concurrency and may therefore not be readable for(int i=0; i < 3; i++) { if(file.exists()) { try { if((list=read(file)) != null) break; } catch(Exception e) { } } Util.sleep(50); } if(list == null) { log.warn("failed reading " + file.getAbsolutePath()); continue; } for(PingData data: list) { if(members == null || members.contains(data.getAddress())) responses.addResponse(data, true); if(local_addr != null && !local_addr.equals(data.getAddress())) addDiscoveryResponseToCaches(data.getAddress(), data.getLogicalName(), data.getPhysicalAddr()); } } } // Format: [name] [UUID] [address:port] [coord (T or F)]. See doc/design/CloudBasedDiscovery.txt for details protected List<PingData> read(File file) throws Exception { return read(new FileInputStream(file)); } @Override protected List<PingData> read(InputStream in) { try { return super.read(in); } finally { reads++; } } /** Write information about all of the member to file (only if I'm the coord) */ protected void writeAll() { Map<Address,PhysicalAddress> cache_contents= (Map<Address,PhysicalAddress>)down_prot.down(new Event(Event.GET_LOGICAL_PHYSICAL_MAPPINGS, false)); List<PingData> list=new ArrayList<>(cache_contents.size()); for(Map.Entry<Address,PhysicalAddress> entry: cache_contents.entrySet()) { Address addr=entry.getKey(); PhysicalAddress phys_addr=entry.getValue(); PingData data=new PingData(addr, true, NameCache.get(addr), phys_addr).coord(addr.equals(local_addr)); list.add(data); } write(list, cluster_name); } protected void write(List<PingData> list, String clustername) { File dir=new File(root_dir, clustername); if(!dir.exists()) dir.mkdir(); String filename=addressToFilename(local_addr); File destination=new File(dir, filename); try { write(list, new FileOutputStream(destination)); } catch(Exception ioe) { log.error(Util.getMessage("AttemptToWriteDataFailedAt") + clustername + " : " + destination.getName(), ioe); deleteFile(destination); } } protected void write(List<PingData> list, OutputStream out) throws Exception { try { super.write(list, out); } finally { writes++; } } protected boolean deleteFile(File file) { boolean result = true; if(log.isTraceEnabled()) log.trace("Attempting to delete file : "+file.getAbsolutePath()); if(file != null && file.exists()) { try { result=file.delete(); log.trace("Deleted file result: "+file.getAbsolutePath() +" : "+result); } catch(Throwable e) { log.error(Util.getMessage("FailedToDeleteFile") + file.getAbsolutePath(), e); } } return result; } protected synchronized void startInfoWriter() { if(info_writer == null || info_writer.isDone()) info_writer=timer.scheduleWithDynamicInterval(new InfoWriter(info_writer_max_writes_after_view, info_writer_sleep_time)); } protected synchronized void stopInfoWriter() { if(info_writer != null) info_writer.cancel(false); } /** Class which calls writeAll() a few times. Started after a view change in which an old coord left */ protected class InfoWriter implements TimeScheduler.Task { protected final int max_writes; protected int num_writes; protected final long sleep_interval; public InfoWriter(int max_writes, long sleep_interval) { this.max_writes=max_writes; this.sleep_interval=sleep_interval; } @Override public long nextInterval() { if(++num_writes > max_writes) return 0; // discontinues this task return Math.max(1000, Util.random(sleep_interval)); } @Override public void run() { writeAll(); } } }