package waymaker.top.android; // Copyright © 2015 Michael Allan. Licence MIT. import android.os.Parcel; import java.util.List; import waymaker.gen.*; import waymaker.spec.VotingID; /** A model of the forest structure of a pollar count. * * @see ‘forest’ */ public final class Forest implements PeersReceiver { static final PolyStator stators = new PolyStator<>(); /////// /** Constructs a Forest. * * @see #pollName() * @see #forestCache() */ public Forest( final String pollName, final ForestCache forestCache ) { this( pollName, forestCache, new NodeCache1(0,/*hasPrecountAdjustments*/false) ); } /** Constructs a Forest from a node cache. * * @see #pollName() * @see #forestCache() * @see #nodeCache() */ public Forest( final String pollName, final ForestCache forestCache, final NodeCache1 nodeCache ) { this.pollName = pollName; this.forestCache = forestCache; this.nodeCache = nodeCache; } /** Constructs a Forest from stored state. * * @see #pollName() * @see #forestCache() * @param inP The parceled state to restore. */ @ThreadRestricted("further KittedPolyStatorSR.openToThread") // for stators.startCtorRestore public Forest( final String pollName, final ForestCache forestCache, final Parcel inP ) { this.pollName = pollName; this.forestCache = forestCache; int s = stators.startCtorRestore( this, inP ); // Node cache. // - - - - - - - assert stators.get(s++) == nodeCache_stator; { nodeCache = new NodeCache1( /* 1 */inP.readInt(), /* 2 */ParcelX.readBoolean(inP) ); // CtorRestore for this config of NodeCache1 construction (q.v.) based on saved state // 3. // - - - NodeCache1.stators.restore( nodeCache, inP ); } // - - - assert s == stators.size(); } // -------------------------------------------------------------------------------------------------- /** The store that holds this forest. */ public ForestCache forestCache() { return forestCache; } private final ForestCache forestCache; /** The store of nodes that defines the forest structure. No content is ever removed or replaced, * but the whole cache may be replaced at any time by an instance with different content. Each * replacement will be signalled by the {@linkplain ForestCache#nodeCacheBell() node cache bell}. */ public NodeCache nodeCache() { return nodeCache; } private NodeCache1 nodeCache; /** Sets the cache of nodes. Does not ring the node cache bell, leaving that to the caller. */ @Warning("non-API") void nodeCache( final NodeCache1 _nodeCache ) { nodeCache = _nodeCache; } @Warning("non-API") NodeCache1 nodeCache1() { return nodeCache; } private static final Object nodeCache_stator = stators.add( new StateSaver() { public void save( final Forest f, final Parcel out ) { // 1. Size. // - - - - - out.writeInt( f.nodeCache.nodeMap.size() ); // 2. Has precount adjustments? // - - - - - - - - - - - - - - - ParcelX.writeBoolean( f.nodeCache.groundUna().precounted() != null, out ); // 3. Cache. // - - - - - - NodeCache1.stators.save( f.nodeCache, out ); } }); /** The identity of the poll that was counted to form this forest. It also identifies the forest. */ public String pollName() { return pollName; } private final String pollName; // - P e e r s - R e c e i v e r -------------------------------------------------------------------- /** {@inheritDoc} Ultimately this may extend a voter list and possibly * {@linkplain CountNode#votersMaybeIncomplete() complete it}. */ public @ThreadSafe void receivePeersResponse( final Object _in ) { // Decode the default response, pending real server counts. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - class Request { final VotingID rootwardID = (VotingID)_in; final int peersStart = 0; // request is for initial peers data // final int peersEndBound ... not yet used below final boolean exhaustsPeers = true; } class Response { final boolean isReal = false; } final Request req = new Request(); final Response res = new Response(); ApplicationX.i().handler().post( new Runnable() { public void run() // on application main thread { /* * * - much of what follows is still pseudo code in extended comments (such as this) pending real server counts */ // Get candidate from node cache. // - - - - - - - - - - - - - - - - final UnadjustedNodeV candidateUna; { final UnadjustedNode node = nodeCache.nodeMap.get( req.rootwardID ); if( node == null ) return; // obsolete response, node no longer cached if( node instanceof UnadjustedNode0 ) return; /* obsolete response, node no longer has unadjusted voters, which are therefore already complete */ candidateUna = (UnadjustedNodeV)node; } final int votersNextOrdinal = candidateUna.votersNextOrdinal(); if( votersNextOrdinal == Integer.MAX_VALUE ) return; // obsolete response, voters now complete if( votersNextOrdinal < req.peersStart ) return; // obsolete response, voters now insufficiently extended (gap versus req.peersStart) // Extend with unadjusted voters from response. // - - - - - - - - - - - - - - - - - - - - - - - boolean areVotersChanged = false; /* * * - for each peer in response - if peer reveals serial inconstency (RepocastSer) - do something to invalidate node cache and escape cleanly from here - if peer.peerOrdinal < candidateUna.votersNextOrdinal ( response overlaps a prior extension - skip - if peer.peerOrdinal >= req.peersEndBound - log as anomaly - skip - get peer from nodeMap - if none - make peer as UnadjustedNode - else remove from nodeCache.outlyingUnadjustedVoters - ensure peer orderly inserted into tail of candidateUna.voters ( cannot already be present ( no need to search in head (pre-existing members), list contracted to grow by pure extension - set areVotersChanged true - if candidateUna.precounted - if no peer.precounted - ensure peer orderly inserted into tail of candidateUna.precounted.voters ( cannot already be present ( no need to search in head... " ( else already added (or to be added) by "precount" extender below */ if( res.isReal ) throw new UnsupportedOperationException(); // for pseudo code above if( req.exhaustsPeers ) { candidateUna.votersNextOrdinal( Integer.MAX_VALUE ); areVotersChanged = true; /* * * - sum outflow of all candidateUna.voters (or inflow if voters are actually roots) - if sum does not equal candidateUna inflow (or poll turnout) ( inconsistency from voters shifting away or unvoting, RepocastSer cannot reliably detect ( will be less than expected, never more - do something to invalidate node cache and escape cleanly from here */ if( res.isReal ) throw new UnsupportedOperationException(); // for pseudo code above } else { /* * * - set candidateUna.votersNextOrdinal to req.peersEndBound */ throw new UnsupportedOperationException(); // for pseudo code above } final boolean isLeaderChanged; if( votersNextOrdinal == 0 ) // this is initial extension covering major voters { assert candidateUna.votersNextOrdinal() != 0; // this happens just once // Extend with any adjusted voters from precount. // - - - - - - - - - - - - - - - - - - - - - - - - final PrecountNode candidatePre = candidateUna.precounted(); if( candidatePre != null ) { final List outlyingVoters = nodeCache.outlyingVotersPre(); for( int v = outlyingVoters.size() - 1; v >= 0; --v ) { final PrecountNode outlyingVoter = outlyingVoters.get( v ); if( outlyingVoter.rootwardInThis().candidate() != candidatePre ) continue; assert outlyingVoter.peerOrdinal() == 0; // major voter, okay for initial extension candidatePre.enlistVoter( outlyingVoter ); ListX.removeUroboros( v, outlyingVoters ); // no longer outlying areVotersChanged = true; } } // else no adjusted candidate, ∴ no adjusted voters // - - - isLeaderChanged = candidateUna.isGround() && !nodeCache.leader().isGround(); // the initial extension of ground voters (forest roots) exposes actual root as leader } else isLeaderChanged = false; // Ring out any changes. // - - - - - - - - - - - - - - - - - - - - - - - - if( areVotersChanged ) forestCache.voterListingBell().ring(); if( isLeaderChanged ) forestCache.nodeCacheBell().ring(); } }); } /////// static { stators.seal(); } }