Peter have you ever constructed a demo of a massively active Javaspace app with and without reflection use for entry deserialization? It might be a great thing to have those numbers to help everyone recognize what you (and I) have seen as impeding implementation/architectural attributes.
Gregg > On Sep 8, 2015, at 11:30 PM, Peter <j...@zeus.net.au> wrote: > > Thanks Greg, > > Was it a case of; because we can't set final fields (well not without a > Permission anyway), that they shouldn't be included in Entry serialized > state, because then we can't deserialize them? > > I've done my best to fix the existing implementations, so hopefully they > won't need further fixes, however, the fixes were very difficult and these > implementations very difficult to reason about, because there is so much > mutable state. In ServiceDiscoveryManager, a thread holds a lock while > waiting for the result of a remote call, there was no solution I could find > to remove this lock. > > To quote Keith Edwards "The Special Semantics of Attributes": > > "All the methods of the object are ignored for purposes of > searching, as are "special" data fields: static, transient, > non-public, or final fields. Likewise all fields that are primitive > types (such as ints and booleans) are ignored; only references to > other objects within an attribute are considered for searching." > > > So our choices are (for River 4.0): > > 1. Break backward compatibility and increase scalability, performance > and reduce bugs, by not ignoring final fields in Entry's, but > instead mandating them. > 2. Or continue full compatibility and live with lower performance, > less scalability and harder to debug code. > > I think there's plenty of time for implementations to prepare for River 4.0, > if we start talking about it now. > > Regards, > > Peter. > > How are these for code comments (from ServiceDiscoveryManager)? > > // Don't like the fact that we're calling foreign code while > // holding an object lock, however holding this lock doesn't > // provide an opportunity for DOS as the lock only relates to > a specific > // ServiceRegistrar and doesn't interact with client code. > matches = proxy.lookup(tmpl, Integer.MAX_VALUE); > > /* The cache must be created inside the listener sync block, > > * otherwise a race condition can occur. This is because the > * creation of a cache results in event registration which > * will ultimately result in the invocation of the serviceAdded() > * method in the cache's listener, and the interruption of any > * objects waiting on the cache's listener. If the notifications > * happen to occur before commencing the wait on the listener > * object (see below), then the wait will never be interrupted > * because the interrupts were sent before the wait() method > * was invoked. Synchronizing on the listener and the listener's > * serviceAdded() method, and creating the cache only after the > * lock has been acquired, together will prevent this situation > * since event registration cannot occur until the cache is > * created, and the lock that allows entry into the serviceAdded() > * method (which is invoked once the events do arrive) is not > * released until the wait() method is invoked . > */ > > /** > * With respect to a given service (referenced by the parameter > * newItem), if either an event has been received from the given lookup > * service (referenced by the proxy parameter), or a snapshot of the > * given lookup service's state has been retrieved, this method > * determines whether the service's attributes have changed, or whether > * a new version of the service has been registered. After the > * appropriate determination has been made, this method applies the > * filter associated with the current cache and sends the appropriate > * local ServiceDiscoveryEvent(s). > * > * This method is called under the following conditions: - when a new > * lookup service is discovered, this method will be called for each > * previously discovered service - when a gap in the events from a > * previously discovered lookup service is discovered, this method will > * be called for each previously discovered service - when a > MATCH_MATCH > * event is received, this method will be called for each previously > * discovered service - when a NOMATCH_MATCH event is received, this > * method will be called for each previously discovered service Note > * that this method is never called when a MATCH_NOMATCH event is > * received; such an event is always handled by the handleMatchNoMatch > * method. > * > * When this method is called, it may send one of the following events > * or combination of events: - a service changed event - a service > * removed event followed by a service added event - a service removed > * event > * > * A service removed event is sent when the service either fails the > * filter, or the filter produces an indefinite result; in which case, > * the service is also discarded. > * > * A service changed event is sent when the service passes the filter, > * and it is determined that the service's attributes have changed. In > * this case, the old and new service proxies are treated as the same > if > * one of the following conditions is met: - this method was called > * because of the receipt of a MATCH_MATCH event - the old and new > * service proxies are byte-wise fully equal (Note that the lookup > * service specification guarantees that the proxies are the same when > a > * MATCH_MATCH event is received.) > * > * A service removed event followed by a service added event is sent > * when the service passes the filter, and the conditions for which a > * service changed event would be considered are not met; that is, this > * method was not called because of the receipt of a MATCH_MATCH event; > * or the old and new service proxies are not byte-wise fully equal. > * > * The if-else-block contained in this method implements the logic just > * described. The parameter matchMatchEvent reflects the pertinent > event > * state that causes this method to be called. That is, either a > * MATCH_MATCH event was received, or it wasn't, (and if it wasn't, > then > * a full byte-wise comparison is performed to determine whether the > * proxies are still the same). > * > * To understand when the 'else' part of the if-else-block is executed, > * consider the following conditions: - there is more than one lookup > * service with which the service registers (ex. LUS-0 and LUS-1) - > * after the service registers with LUS-0, a NOMATCH_MATCH event is > * received and handled (so the service is now known to the cache) - > * before the service registers with LUS-1, the service is replaced > with > * a new version - the NOMATCH_MATCH event resulting from the service's > * registration with LUS-1 is received BEFORE receiving the > * MATCH_NOMATCH/NOMATCH_MATCH event sequence that will ultimately > * result from the re-registration of that new version with LUS-0 When > * the above conditions occur, the NOMATCH_MATCH event that resulted > * from the service's registration with LUS-1 will cause this method to > * be invoked and the proxies to be fully compared (because the event > * was not a MATCH_MATCH event); and since the old service proxy and > the > * new service proxy will not be fully equal, the else part of the > * if-else-block will be executed. > * > * This method applies the filter only after the above comparisons and > * determinations have been completed. > */ > > On 9/09/2015 1:40 PM, Greg Trasuk wrote: >>> On Sep 8, 2015, at 10:40 PM, Peter<j...@zeus.net.au> wrote: >>> >>> On 8/09/2015 11:26 PM, Greg Trasuk wrote: >>>> That’s the current state. Changing (e.g. by enforcing a builder pattern >>>> or something) would add unneeded complexity for the user if you ask me. >>> Yes, I agree, a constructor is suitable, keep it simple. >>> >>>> In fact, the fact that the Entry fields are non-final is the way Jini >>>> knows it’s a field and not a constant. i.e. in the AbstractEntry docs, it >>>> specifically says "The entry fields of an Entry are its public, >>>> non-primitive, non-static, non-transient, non-final fields." >>> There's no code that inspects the field and checks whether it's final, not >>> within River that I'm aware of, feel free to show me where if I'm wrong :). >> com.sun.jini.outrigger.EntryRep. Look for ‘Modifier.FINAL’ in the >> ‘usableField(…)’ method. There are 12 other uses of ‘FINAL’ in River 2.2.2. >> Not to mention any external product (Blitz, Rio, who knows what) that are >> designed around the Entry specification. >> >> The EntryRep you posted below is com.sun.jini.reggie.EntryRep. You’ll >> notice that it calls com.sun.jini.reggie.ClassMapper, which also filters >> final fields out of the comparisons and mappings. >> >>> The reason these fields are non final, is so they can be set with >>> reflection, this is a detail of the Entry specification we should look at >>> changing. >>> >> The reason they’re non-final is so that services like Reggie and Outrigger >> know what constitutes an Entry field that they might need to match. Entry >> fields are used to form the templates for matching entries. >> >>> Entry's are not subject to the usual serialization rules. All fields in an >>> Entry in superclass to subclass order are stored in an EntryRep (appended). >>> At present these are set after construction using reflection, however a >>> constructor that accepts an array parameter, will allow the child most >>> class to pass that array up through all constructors to reconstruct the >>> Entry without using reflection (magnitudes faster performance wise). >>> >>> Eliminating the use of reflection during deserialization will increase >>> performance, and immutability increases scalability. >>> >>>> Where do we use entries? Typically in doing Service registrations and >>>> lookups. Most users can understand that the Entry is going to be >>>> serialized and used remotely. >>> Clients of Javaspaces and utility classes too, like JoinManager and >>> ServiceDiscoveryManager. >>>> Add to documentation? Sure. Change the API? Maybe add an optional >>>> builder-style object. (e.g. create a StatusBuilder that acts as a bean >>>> and generates Status entries). But even so, it seems like a small enough >>>> part of using Jini to not bother much with. >>> Due to the way the current Entry spec works, you can't add a field to an >>> Entry without breaking compatibility with subclassses. >>> >>> So the proposed change would be a new public constructor, that accepts an >>> array, containing field object values in the order that fields occur and >>> that all fields be final. >>> >>> I'd also propose including a warning that once published the order and >>> number of fields in an Entry should not be changed if it can be extended, >>> otherwise if there's a chance that additional fields might need to be >>> appended at a later date, a reccommendation that the class be made final, >>> so it can't be subclassed. >>> >>> So in other words, the following changes will break an Entry: >>> >>> 1. A change in the class heirarchy. >>> 2. A change in the order or number or type of fields. >>> >>> An Entry is best thought of an interface definition for a defined group of >>> objects in serial form. >>> >>>> >>>> Another way to think about it is that Entries don’t mean much until you >>>> send them somewhere. So you need to make sure they’re all setup and >>>> everything they refer to is stable when you send them. To me, that’s a >>>> user-understanding item that’s difficult to enforce through code. >>> But because Entry fields are mutable and unsynchronized, and used in >>> utility classes such as ServiceDiscoveryManager, it becomes difficult to >>> manage when Entry's are passed around between threads. To submit an Entry >>> to a service, it is passed to a thread pool where it becomes a serialized >>> method invocation. >>> >>> To ensure the changes made by one thread are visible to another, the Entry >>> must be published safely, otherwise the changes can't be guaranteed visible >>> between threads. There is no synchronization on Entry field access, so >>> this makes it very difficult to reason about an Entry, one strategy I've >>> adopted is defensive copying, however all this does, is guarantee that any >>> modifications made by client code, doesn't affect a copy in >>> ServiceDiscoveryManager (for example), there is a memory usage cost, >>> because internal copies can't be shared with client ServiceItemFilter’s >> Typical usage of a ServiceDiscoveryManager, for instance, would be if I want >> to perform a lookup I’ll create a ServiceTemplate that contains the Entries >> I want matched. If I want to create a LookupCache, I’ll pass in a >> ServiceTemplate to the createLookupCache method. >> >> If you have a separate thread that messes around with those entries while >> the lookup is in process, or while the LookupCache is active, that’s a >> client error - you deserve what you get. >> >> If ServiceDiscoveryManager does it wrong, we need to fix >> ServiceDiscoveryManager. >> >>> . >>> >>> Reasoning about shared state becomes much simpler if Entry's are immutable >>> and the upside is scalability and performance improves, especially for >>> clients of lookup services deserializing a lot of Entry objects. >>> >> Entries aren’t intended to encapsulate shared state in any widely concurrent >> way. They’re intended to be created on a thread, and then sent over the >> network. If there is shared state inside a given service, that’s the >> concern of the service implementation. Does Outrigger handle its >> concurrency correctly? I don’t know- I’ve never looked. If it doesn’t then >> we need to fix Outrigger, not change the Entry specification. >> >> It’s good that we’re having these discussions, so we can make sure that the >> real-world usages get taken into account before we go altering the >> specifications. >> >> Cheers, >> >> Greg Trasuk. >> >> >>> /** >>> * An EntryRep contains the fields of an Entry packaged up for >>> * transmission between client-side proxies and the registrar server. >>> * Instances are never visible to clients, they are private to the >>> * communication between the proxies and the server. >>> *<p> >>> * This class only has a bare minimum of methods, to minimize >>> * the amount of code downloaded into clients. >>> * >>> * @author Sun Microsystems, Inc. >>> * >>> */ >>> class EntryRep implements Serializable, Cloneable { >>> >>> private static final long serialVersionUID = 2L; >>> >>> /** >>> * The Class of the Entry converted to EntryClass. >>> * >>> * @serial >>> */ >>> public EntryClass eclass; >>> /** >>> * The codebase of the entry class. >>> * >>> * @serial >>> */ >>> public String codebase; >>> /** >>> * The public fields of the Entry, each converted as necessary to >>> * a MarshalledWrapper (or left as is if of known java.lang immutable >>> * type). The fields are in super- to subclass order. >>> * >>> * @serial >>> */ >>> public Object[] fields; >>> >>> /** >>> * Converts an Entry to an EntryRep. Any exception that results >>> * is bundled up into a MarshalException. >>> */ >>> public EntryRep(Entry entry) throws RemoteException { >>> EntryClassBase ecb = ClassMapper.toEntryClassBase(entry.getClass()); >>> eclass = ecb.eclass; >>> codebase = ecb.codebase; >>> try { >>> EntryField[] efields = ClassMapper.getFields(entry.getClass()); >>> fields = new Object[efields.length]; >>> for (int i = efields.length; --i>= 0; ) { >>> EntryField f = efields[i]; >>> Object val = f.field.get(entry); >>> if (f.marshal&& val != null) >>> val = new MarshalledWrapper(val); >>> fields[i] = val; >>> } >>> } catch (IOException e) { >>> throw new MarshalException("error marshalling arguments", e); >>> } catch (IllegalAccessException e) { >>> throw new MarshalException("error marshalling arguments", e); >>> } >>> } >>> >>> /** >>> * Convert back to an Entry. If the Entry cannot be constructed, >>> * null is returned. If a field cannot be unmarshalled, it is set >>> * to null. >>> */ >>> public Entry get() { >>> try { >>> Class clazz = eclass.toClass(codebase); >>> EntryField[] efields = ClassMapper.getFields(clazz); >>> Entry entry = (Entry)clazz.newInstance(); >>> for (int i = efields.length; --i>= 0; ) { >>> Object val = fields[i]; >>> EntryField f = efields[i]; >>> Field rf = f.field; >>> try { >>> if (f.marshal&& val != null) >>> val = ((MarshalledWrapper) val).get(); >>> rf.set(entry, val); >>> } catch (Throwable e) { >>> if (e instanceof IllegalArgumentException) { >>> // fix 4872566: work around empty exception message >>> String msg = "unable to assign " + >>> ((val != null) ? >>> "value of type " + val.getClass().getName() : >>> "null") + >>> " to field " + rf.getDeclaringClass().getName() + >>> "." + rf.getName() + " of type " + >>> rf.getType().getName(); >>> e = new ClassCastException(msg).initCause(e); >>> } >>> RegistrarProxy.handleException(e); >>> } >>> } >>> return entry; >>> } catch (Throwable e) { >>> RegistrarProxy.handleException(e); >>> } >>> return null; >>> } >>> >>> /** >>> * We don't need this in the client or the server, but since we >>> * redefine equals we provide a minimal hashCode that works. >>> */ >>> public int hashCode() { >>> return eclass.hashCode(); >>> } >>> >>> /** >>> * EntryReps are equal if they have the same class and the fields >>> * are pairwise equal. This is really only needed in the server, >>> * but it's very convenient to have here. >>> */ >>> public boolean equals(Object obj) { >>> if (obj instanceof EntryRep) { >>> EntryRep entry = (EntryRep)obj; >>> if (!eclass.equals(entry.eclass) || >>> fields.length != entry.fields.length) >>> return false; >>> for (int i = fields.length; --i>= 0; ) { >>> if ((fields[i] == null&& entry.fields[i] != null) || >>> (fields[i] != null&& !fields[i].equals(entry.fields[i]))) >>> return false; >>> } >>> return true; >>> } >>> return false; >>> } >>> >>> /** >>> * Deep clone (which just means cloning the fields array too). >>> * This is really only needed in the server, but it's very >>> * convenient to have here. >>> */ >>> public Object clone() { >>> try { >>> EntryRep entry = (EntryRep)super.clone(); >>> entry.fields = (Object[])entry.fields.clone(); >>> return entry; >>> } catch (CloneNotSupportedException e) { >>> throw new InternalError(); >>> } >>> } >>> >>> /** >>> * Converts an array of Entry to an array of EntryRep. If needCodebase >>> * is false, then the codebase of every EntryRep will be null. >>> */ >>> public static EntryRep[] toEntryRep(Entry[] entries, boolean >>> needCodebase) >>> throws RemoteException >>> { >>> EntryRep[] reps = null; >>> if (entries != null) { >>> reps = new EntryRep[entries.length]; >>> for (int i = entries.length; --i>= 0; ) { >>> if (entries[i] != null) { >>> reps[i] = new EntryRep(entries[i]); >>> if (!needCodebase) >>> reps[i].codebase = null; >>> } >>> } >>> } >>> return reps; >>> } >>> >>> /** Converts an array of EntryRep to an array of Entry. */ >>> public static Entry[] toEntry(EntryRep[] reps) { >>> Entry[] entries = null; >>> if (reps != null) { >>> entries = new Entry[reps.length]; >>> for (int i = reps.length; --i>= 0; ) { >>> entries[i] = reps[i].get(); >>> } >>> } >>> return entries; >>> } >>> } >>> >> >