package org.radargun.service; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.management.ManagementFactory; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicInteger; import javax.management.*; import org.jgroups.*; import org.jgroups.blocks.*; import org.jgroups.util.RspList; import org.jgroups.util.Util; import org.radargun.Service; import org.radargun.config.Property; import org.radargun.logging.Log; import org.radargun.logging.LogFactory; import org.radargun.traits.*; import org.radargun.utils.Utils; /** * Plugin measuring the costs of remote gets and puts with JGroups, with regular arguments passed by RadarGun. * However, a GET returns a <em>prefabricated</em> value (no cache handling) and a PUT simply invokes the remote call, * but doesn't add anything to a hashmap.<p/> * The point of this plugin is to measure the overhead of Infinispan's cache handling; it is a base line to the * Infinispan plugin. The Infinispan plugin should be slower than the JGroups plugin, but the difference should always * be constant, regardless of the cluster size.<p/> * Properties, such as the size of the layload for gets, and the number of owners of a key, can be * defined in jgroups.properties. * * Default behavior of puts and gets (mimicking) Infinispan): * - A get is sent to the all owners and the call returns after the *first* response has been received. A get which * has the local node included returns immediately (mimicking a local read) and no RPC is sent. * * - A put is sent to the primary owner P. If P == self --> no-op. P then synchronously sends an update() to * the backup(s) (minus self). * * * @author Radim Vansa <rvansa@redhat.com> * @author Bela Ban */ @Service(doc = "JGroupsService faking cache operations") public class JGroups36Service extends ReceiverAdapter implements Lifecycle, Clustered, BasicOperations.Cache { protected static Log log = LogFactory.getLog(JGroups36Service.class); private static final Method[] METHODS = new Method[7]; protected static final short GET = 0; protected static final short CONTAINS_KEY = 1; protected static final short PUT = 2; protected static final short GET_AND_PUT = 3; protected static final short REMOVE = 4; protected static final short GET_AND_REMOVE = 5; protected static final short PUT_AND_FORWARD = 6; protected JChannel ch; protected RpcDispatcher disp; protected volatile Address localAddr; protected volatile int myRank; // rank of current member in view protected volatile List<Address> members = Collections.emptyList(); protected List<Membership> membershipHistory = new ArrayList<>(); @Property(doc = "Number of nodes where the writes will be replicated.") protected int numOwners = 2; @Property(doc = "Controls use of the DONT_BUNDLE flag. Default is true.") protected boolean bundle = true; @Property(doc = "Controls use of the FC flag. Default is true.") protected boolean flowControl = true; @Property(doc = "Controls use of the OOB flag. Default is true.") protected boolean oob = true; @Property(doc = "Controls use of anycasting flag in RequestOptions. Default is true.") protected boolean anycasting = true; @Property(name = "file", doc = "Configuration file for JGroups.", deprecatedName = "config") protected String configFile; @Property(doc = "When enabled, a put is sent to the primary which (synchronously) " + "replicates it to the backup(s). Otherwise the put is sent to all owners and the call return on the first reply." + " Default is true (Infinispan 7.x behavior). Setting this to false will reduce the cost of 4x latency to 2x (faster)") protected boolean primaryReplicatesPuts = true; protected String name; protected volatile Object lastValue = new byte[1000]; protected RequestOptions getOptions, putOptions, putOptionsWithFilter; protected final AtomicInteger localReads = new AtomicInteger(0); // number of local reads (no RPCs) static { try { METHODS[GET] = JGroups36Service.class.getMethod("getFromRemote", Object.class); METHODS[CONTAINS_KEY] = JGroups36Service.class.getMethod("containsKeyFromRemote", Object.class); METHODS[PUT] = JGroups36Service.class.getMethod("putFromRemote", Object.class, Object.class); METHODS[GET_AND_PUT] = JGroups36Service.class.getMethod("getAndPutFromRemote", Object.class, Object.class); METHODS[REMOVE] = JGroups36Service.class.getMethod("removeFromRemote", Object.class); METHODS[GET_AND_REMOVE] = JGroups36Service.class.getMethod("getAndRemoveFromRemote", Object.class); METHODS[PUT_AND_FORWARD] = JGroups36Service.class.getMethod("putFromRemote", Object.class, Object.class, int.class); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } public JGroups36Service() { } public JGroups36Service(String configFile, String name) { this.configFile = configFile; this.name = name; } public JGroups36Service configFile(String file) { this.configFile = file; return this; } public JGroups36Service name(String name) { this.name = name; return this; } public JGroups36Service numOwners(int num) { this.numOwners = num; return this; } @ProvidesTrait public JGroups36Service getSelf() { return this; } @ProvidesTrait public BasicOperations createOperations() { return new BasicOperations() { @Override public <K, V> Cache<K, V> getCache(String cacheName) { return JGroups36Service.this; } }; } @Override public void start() { this.getOptions = new RequestOptions(ResponseMode.GET_FIRST, 20000, anycasting, null); if (oob) { getOptions.setFlags(Message.Flag.OOB); } if (!bundle) { getOptions.setFlags(Message.Flag.DONT_BUNDLE); } if (!flowControl) { getOptions.setFlags(Message.Flag.NO_FC); } this.putOptions = new RequestOptions(ResponseMode.GET_FIRST, 20000, true, null); // uses anycasting if (oob) { putOptions.setFlags(Message.Flag.OOB); } if (!bundle) { putOptions.setFlags(Message.Flag.DONT_BUNDLE); } if (!flowControl) { putOptions.setFlags(Message.Flag.NO_FC); } putOptionsWithFilter = new RequestOptions(putOptions).setRspFilter(new FirstNonNullResponse()); log.debugf("numOwners=%d, config=%s, getOptions=%s, putOptions=%s\n", numOwners, configFile, getOptions, putOptions); log.info("Loading JGroups form: " + org.jgroups.Version.class.getProtectionDomain().getCodeSource().getLocation()); log.info("JGroups version: " + org.jgroups.Version.printDescription()); try { ch = new JChannel(configFile).name(name); disp = new RpcDispatcher(ch, null, this, this); disp.setMethodLookup(id -> METHODS[id]); ch.connect("x"); } catch (Exception e) { throw new RuntimeException(e); } localAddr = ch.getAddress(); myRank = Util.getRank(ch.getView(), localAddr) - 1; } @Override public void stop() { Util.close(ch); synchronized (this) { membershipHistory.add(Membership.empty()); } } @Override public boolean isRunning() { return ch != null && ch.isConnected(); } public Object getFromRemote(Object key) { assert key != null; return lastValue; } public boolean containsKeyFromRemote(Object key) { assert this != null && key != null; return true; } public void putFromRemote(Object key, Object value) { if (key != null) { lastValue = value; } } /** * Applies a put() and forwards it to targets * * @param key * @param val * @param excludeRank The rank to be excluded from the backups (the originator of the put) */ public void putFromRemote(Object key, Object val, int excludeRank) { putFromRemote(key, val); // forward to backup owners if (excludeRank == -1) return; List<Address> backupOwners = pickBackups(myRank, excludeRank); if (backupOwners == null || backupOwners.isEmpty()) return; if (backupOwners.size() == 1) invoke(backupOwners.get(0), new MethodCall(PUT, key, val), putOptions); else invoke(backupOwners, new MethodCall(PUT, key, val), putOptions); } public Object getAndPutFromRemote(Object key, Object value) { assert key != null; Object last = lastValue; lastValue = value; return last; } public boolean removeFromRemote(Object key) { assert this != null && key != null; return true; } public Object getAndRemoveFromRemote(Object key) { assert key != null; return lastValue; } protected Object read(MethodCall methodCall) { List<Address> targets = pickReadTargets(); if (targets == null) { // self was element of the picked members -> local read, no RPC localReads.incrementAndGet(); return lastValue; } return invoke(targets, methodCall, getOptions).getFirst(); } public Object write(MethodCall methodCall) { Collection<Address> targets = pickWriteTargets(); return invoke(targets, methodCall, putOptionsWithFilter).getFirst(); } @Override public Object get(Object key) { return read(new MethodCall(GET, key)); } @Override public boolean containsKey(Object key) { return (Boolean) read(new MethodCall(CONTAINS_KEY, key)); } @Override public void put(Object key, Object value) { if (this.primaryReplicatesPuts) { List<Address> owners = pickTargets(false, false); Address primary = owners.remove(0); owners.remove(localAddr); // backups shouldn't forward back to us - we already applied the put int excludeRank = owners.isEmpty() ? -1 : myRank; if (primary.equals(localAddr)) putFromRemote(key, value, excludeRank); else invoke(primary, new MethodCall(PUT_AND_FORWARD, key, value, excludeRank), putOptions); } else write(new MethodCall(PUT, key, value)); } @Override public Object getAndPut(Object key, Object value) { return write(new MethodCall(GET_AND_PUT, key, value)); } @Override public boolean remove(Object key) { return (Boolean) write(new MethodCall(REMOVE, key)); } @Override public Object getAndRemove(Object key) { return write(new MethodCall(GET_AND_REMOVE, key)); } public void clear() { lastValue = null; } public void viewAccepted(View newView) { this.members = newView.getMembers(); this.myRank = Util.getRank(newView, localAddr) - 1; ArrayList<Member> mbrs = new ArrayList<>(newView.getMembers().size()); boolean coord = true; for (Address address : newView.getMembers()) { mbrs.add(new Member(address.toString(), ch.getAddress().equals(address), coord)); coord = false; } synchronized (this) { membershipHistory.add(Membership.create(mbrs)); } } @Override public boolean isCoordinator() { View view = ch.getView(); return view == null || view.getMembers() == null || view.getMembers().isEmpty() || ch.getAddress().equals(view.getMembers().get(0)); } @Override public synchronized Collection<Member> getMembers() { if (membershipHistory.isEmpty()) return null; return membershipHistory.get(membershipHistory.size() - 1).members; } @Override public synchronized List<Membership> getMembershipHistory() { return new ArrayList<>(membershipHistory); } // 1-m invocation protected RspList<Object> invoke(Collection<Address> targets, MethodCall methodCall, RequestOptions opts) { try { return disp.callRemoteMethods(targets, methodCall, opts); } catch (Exception e) { throw new RuntimeException(e); } } // 1-1 invocation protected Object invoke(Address target, MethodCall methodCall, RequestOptions opts) { try { return disp.callRemoteMethod(target, methodCall, opts); } catch (Exception e) { throw new RuntimeException(e); } } /** * Picks a random primary plus numOwners-1 backup members from the membership * * @return The list of primary and backup members, or null if self was element of that list (local reads) */ protected List<Address> pickReadTargets() { return pickTargets(true, false); } /** * Picks a random primary and numOwners-1 backups, but removes self. So if we pick {A,B} (self=A), then the RPC will * only go to B. * * @return */ protected List<Address> pickWriteTargets() { return pickTargets(false, true); // exclude self } /** * Picks numOwners targets in range [i .. i+numOwners-1] where i is a random index * * @return A list of members (primary plus backup(s)), or null if returnNullOnSelfInclusion is true and self is in * the list */ protected List<Address> pickTargets(boolean returnNullOnSelfInclusion, boolean skipSelf) { List<Address> mbrs = this.members; int size = mbrs.size(); int startIndex = ThreadLocalRandom.current().nextInt(size); int numTargets = Math.min(numOwners, size); List<Address> targets = new ArrayList<>(numTargets); for (int i = 0; i < numTargets; i++) { int index = (startIndex + i) % size; if (index == myRank) { if (returnNullOnSelfInclusion) return null; if (skipSelf) continue; } Address target = mbrs.get(index); targets.add(target); // we cannot have dupes because numTargets cannot be > size (due to the min() above) } return targets; } /** * Picks backup owners, based on a starting index * * @param primaryRank The rank of the primary in the current view. Start with (primaryRank+1) % size * @param excludeRank Exclude primary if true * @return */ protected List<Address> pickBackups(int primaryRank, int excludeRank) { List<Address> mbrs = this.members; int size = mbrs.size(); int startIndex = primaryRank + 1; int numTargets = Math.min(numOwners - 1, size); List<Address> targets = new ArrayList<>(numTargets); for (int i = 0; i < numTargets; i++) { int index = (startIndex + i) % size; if (index == excludeRank) continue; Address target = mbrs.get(index); targets.add(target); // we cannot have dupes because numTargets cannot be > size (due to the min() above) } return targets; } @ProvidesTrait ConfigurationProvider getConfigurationProvider() { return new ConfigurationProvider() { @Override public Map<String, Properties> getNormalizedConfigs() { return Collections.singletonMap("jgroups", dumpProperties()); } @Override public Map<String, byte[]> getOriginalConfigs() { InputStream stream = null; try { stream = getClass().getResourceAsStream(configFile); if (stream == null) { stream = new FileInputStream(configFile); } return Collections.singletonMap(configFile, Utils.readAsBytes(stream)); } catch (IOException e) { log.error("Cannot read configuration file " + configFile, e); return Collections.EMPTY_MAP; } finally { Utils.close(stream); } } }; } protected Properties dumpProperties() { Properties p = new Properties(); try { MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); String objName = String.format("jboss.infinispan:type=protocol,cluster=\"%s\",protocol=*", ch.getClusterName()); Set<ObjectInstance> beanObjs = mbeanServer.queryMBeans(new ObjectName(objName), null); if (beanObjs.isEmpty()) { log.error("no JGroups protocols found"); return p; } for (ObjectInstance beanObj : beanObjs) { ObjectName protocolObjectName = beanObj.getObjectName(); MBeanInfo protocolBean = mbeanServer.getMBeanInfo(protocolObjectName); String protocolName = protocolObjectName.getKeyProperty("protocol"); for (MBeanAttributeInfo info : protocolBean.getAttributes()) { String propName = info.getName(); Object propValue = mbeanServer.getAttribute(protocolObjectName, propName); p.setProperty(protocolName + "." + propName, propValue == null ? "null" : propValue.toString()); } } return p; } catch (Exception e) { log.error("Error while dumping JGroups config as properties", e); return p; } } /** * Terminates after the first non-null response */ protected static class FirstNonNullResponse implements RspFilter { protected boolean receivedNonNullRsp; public boolean isAcceptable(Object response, Address sender) { if (response != null) { receivedNonNullRsp = true; return true; } return false; } public boolean needMoreResponses() { return !receivedNonNullRsp; } } }