All Downloads are FREE. Search and download functionalities are using the official Maven repository.

convex.core.data.Ref Maven / Gradle / Ivy

The newest version!
package convex.core.data;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.function.Consumer;

import convex.core.Constants;
import convex.core.data.prim.CVMBool;
import convex.core.data.util.BlobBuilder;
import convex.core.exceptions.BadFormatException;
import convex.core.exceptions.InvalidDataException;
import convex.core.exceptions.MissingDataException;
import convex.core.store.AStore;
import convex.core.store.Stores;
import convex.core.util.Trees;

/**
 * Class representing a smart reference to a decentralised data value.
 * 
 * "The greatest trick the Devil ever pulled was convincing the world he didn’t
 * exist." - The Usual Suspects
 * 
 * A Ref itself is not a Cell, but may be contained within a Cell, in which case
 * the Cell class must implement IRefContainer in order to persist and update
 * contained Refs correctly
 * 
 * Refs include a status that indicates the level of validation proven. It is
 * important not to rely on the value of a Ref until it has a sufficient status
 * - e.g. a minimum status of PERSISTED is required to be able to guarantee
 * walking an entire nested data structure.
 * 
 * Guarantees: 
 * - O(1) access to the Hash value, cached on first access 
 * - O(1) access to the referenced object (though may required hitting storage if not cached) 
 * - Indirectly referenced values may be collected by the garbage collector, with the assumption that they can be retrieved from storage if required
 *
 * @param  Type of stored value
 */
public abstract class Ref extends AObject implements Comparable>, IWriteable, IValidated {

	/**
	 * Ref status indicating the status of this Ref is unknown. This is the default
	 * for new Refs
	 */
	public static final int UNKNOWN = 0;

	/**
	 * Ref status indicating the Ref has been shallowly persisted in long term
	 * storage. The Ref can be made soft, and retrieved from storage if needed. No
	 * guarantee about the existence / status of any child objects.
	 */
	public static final int STORED = 1;

	/**
	 * Ref status indicating the Ref has been deeply persisted in long term storage.
	 * The Ref and its children can be assumed to be accessible for the life of the
	 * storage subsystem execution. Embedded cells can assume persisted at minimum.
	 */
	public static final int PERSISTED = 2;

	/**
	 * Ref status indicating the Ref has been both persisted and validated as genuine
	 * valid CVM data.
	 */
	public static final int VALIDATED = 3;

	/**
	 * Ref status indicating the Ref has been shared by this peer in an announced
	 * Belief. This means that the Peer has a commitment to maintain this data
	 */
	public static final int ANNOUNCED = 4;
	
	/**
	 * Ref status indicating the value is marked in the store for GC copying. Marked values
	 * are retained until next GC cycle
	 */
	public static final int MARKED = 5;
	
	/**
	 * Ref status indicating the Ref in internal data that should never be discarded
	 */
	public static final int INTERNAL = 15;

	

	/**
	 * Maximum Ref status
	 */
	public static final int MAX_STATUS = ANNOUNCED;
	
	/**
	 * Mask for Ref flag bits representing the Status
	 */
	public static final int STATUS_MASK = 0x0F;
	
	/**
	 * Mask bit for a proven embedded value
	 */
	public static final int KNOWN_EMBEDDED_MASK = 0x10;
	
	/**
	 * Mask bit for a proven non-embedded value
	 */
	public static final int NON_EMBEDDED_MASK = 0x20;

	/**
	 * Mask for embedding status
	 */
	public static final int EMBEDDING_MASK = KNOWN_EMBEDDED_MASK | NON_EMBEDDED_MASK;
	
	/**
	 * Mask bit for verified data, especially signatures
	 */
	public static final int VERIFIED_MASK = 0x40;

	/**
	 * Mask bit for bad data, especially signatures proved invalid
	 */
	public static final int BAD_MASK = 0x80;
	
	/**
	 * Mask bit for bad data, especially signatures proved invalid
	 */
	public static final int VERIFICATION_MASK = VERIFIED_MASK | BAD_MASK;

	/**
	 * Flags for valid embedded values, typically used on creation
	 */
	public static final int VALID_EMBEDDED_FLAGS=PERSISTED|KNOWN_EMBEDDED_MASK|VERIFIED_MASK;
	
	/**
	 * Flags for valid embedded values, typically used on creation
	 */
	public static final int INTERNAL_FLAGS=INTERNAL|VERIFIED_MASK;

	
	/**
	 * Ref status indicating that the Ref refers to data that has been proven to be invalid
	 */
	public static final int INVALID = -1;

	/**
	 * Ref for null value. Important because we can't persist this, since null
	 * collides with the result of an empty soft reference.
	 */
	public static final RefDirect NULL_VALUE = RefDirect.create(null, Hash.NULL_HASH, INTERNAL_FLAGS);

	public static final RefDirect TRUE_VALUE = RefDirect.create(CVMBool.TRUE, Hash.TRUE_HASH, INTERNAL_FLAGS);
	public static final RefDirect FALSE_VALUE = RefDirect.create(CVMBool.FALSE, Hash.FALSE_HASH, INTERNAL_FLAGS);

	/**
	 * Length of an external Reference encoding. Will be a tag byte plus the Hash length
	 */
	public static final int INDIRECT_ENCODING_LENGTH = 1+Hash.LENGTH;

	/**
	 * Hash of the serialised representation of the value Computed and stored upon
	 * demand.
	 */
	protected Hash hash;

	/**
	 * Flag values including Status of this Ref. See public Ref status constants.
	 * 
	 * May be incremented atomically in the event of validation, proven storage.
	 */
	protected int flags;

	protected Ref(Hash hash, int flags) {
		this.hash = hash;
		this.flags = flags;
	}

	/**
	 * Gets the status of this Ref
	 * 
	 * @return UNKNOWN, PERSISTED, VERIFIED, ACCOUNCED or INVALID Ref status
	 *         constants
	 */
	public int getStatus() {
		return flags&STATUS_MASK;
	}
	
	/**
	 * Gets flags with an updated status
	 * @param newStatus New status to apply to flags
	 * @return Updated flags (does not change this Ref)
	 */
	public int flagsWithStatus(int newStatus) {
		return (flags&~STATUS_MASK)|(newStatus&STATUS_MASK);
	}
	
	/**
	 * Gets the flags for this Ref
	 * 
	 * @return flag int value
	 */
	public int getFlags() {
		return flags;
	}

	/**
	 * Return a Ref that has the given status, at minimum. If status was updated, returns a new Ref
	 * 
	 * Assumes any necessary changes to storage will be made separately. 
	 * SECURITY: Dangerous if misused since may invalidate storage assumptions
	 * @param newStatus New status to apply to Ref
	 * @return Updated Ref (may be same Ref is status unchanged)
	 */
	public Ref withMinimumStatus(int newStatus) {
		newStatus&=STATUS_MASK;
		int status=getStatus();
		if (status >= newStatus) return this;
		if (status > MAX_STATUS) {
			throw new IllegalArgumentException("Ref status not recognised: " + newStatus);
		}
		int newFlags=(flags&(~STATUS_MASK))|newStatus;
		return withFlags(newFlags);
	}

	/**
	 * Return a a similar Ref of the same type with updated flags. Creates a new Ref if lags have changed.
	 * @param newFlags New flags to set
	 * @return Updated Ref
	 */
	public abstract Ref withFlags(int newFlags);

	/**
	 * Gets the value from this Ref.
	 * 
	 * Important notes: - May throw a MissingDataException if the data does not
	 * exist in available storage - Will return null if and only if the Ref refers
	 * to the null value
	 * 
	 * @return The value contained in this Ref
	 */
	public abstract T getValue();

	@Override
	public int hashCode() {
		return getHash().hashCode();
	}
	
	@Override
	public boolean print(BlobBuilder bb, long limit) {
		bb.append("#ref {:hash #hash ");
		bb.append((hash==null)?"nil":hash.toString());
		bb.append(", :flags ");
		bb.append(Integer.toString(flags));
		bb.append("}");
		return bb.check(limit);
	}

	@Override
	public String toString() {
		BlobBuilder sb = new BlobBuilder();
		print(sb,Constants.PRINT_LIMIT);
		return Strings.create(sb.toBlob()).toString();
	}

	@SuppressWarnings("unchecked")
	@Override
	public boolean equals(Object o) {
		if (!(o instanceof Ref)) return false;
		return equals((Ref) o);
	}
	
	/**
	 * Checks if two Ref Values are equal. Equality is defined as referring to the
	 * same data, i.e. have an identical hash.
	 * 
	 * @param a The Ref to compare with
	 * @return true if Refs have the same value, false otherwise
	 */
	public abstract boolean equals(Ref a);

	@Override
	public int compareTo(Ref a) {
		if (this == a) return 0;
		return getHash().compareTo(a.getHash());
	}

	/**
	 * Gets the Hash of this ref's value.
	 * 
	 * @return Hash of the value
	 */
	public abstract Hash getHash();
	
	/**
	 * Gets the Hash of this ref's value, or null if not yet computed
	 * 
	 * @return Hash of the value, or null if not yet computed
	 */
	public final Hash cachedHash() {
		return hash;
	}

	/**
	 * Returns a direct Ref wrapping the given value. Does not perform any Ref
	 * lookup in stores etc.
	 * 
	 * @param value Value to wrap in the Ref
	 * @return New Ref wrapping the given value.
	 */
	@SuppressWarnings("unchecked")
	public static  Ref get(T value) {
		if (value==null) return (Ref) NULL_VALUE;
		return value.getRef();
	}

	/**
	 * Creates a RefSoft using a specific Hash. Fetches the actual value lazily from the
	 * current thread's store on demand.
	 * 
	 * Internal soft reference may be initially empty: This Ref might not have
	 * available data in the store, in which case calls to getValue() may result in
	 * a MissingDataException
	 * 
	 * WARNING: Does not mark as either embedded or non-embedded, as this might be a top level 
	 * entry in the store. isEmbedded() will query the store to determine status.
	 * 
	 * @param hash The hash value for this Ref to refer to
	 * @return Ref for the specific hash.
	 */
	public static  RefSoft forHash(Hash hash) {
		return RefSoft.createForHash(hash);
	}
	
	public Ref markEmbedded(boolean isEmbedded) {
		int newFlags=mergeFlags(flags,(isEmbedded?KNOWN_EMBEDDED_MASK:NON_EMBEDDED_MASK));
		flags=newFlags;
		return this;
	}
	
	/**
	 * Sets the Flags for this Ref. WARNING: caller must have performed any necessary validation
	 * @param newFlags Flags to set
	 * @return Updated Ref
	 */
	public Ref setFlags(int newFlags) {
		flags=newFlags;
		return this;
	}
	
	/**
	 * Reads a ref from the given Blob position. Assumes no tag.
	 * 
	 * Marks as non-embedded, since only non-embedded cells should be encoded this way
	 * 
	 * @param b Blob containing the data to read at the current position
	 * @param pos position in Blob to read
	 * @return Ref read from ByteBuffer
	 * @throws BadFormatException If there are insufficient bytes to read a full Ref
	 */
	public static  Ref readRaw(Blob b, int pos) throws BadFormatException {
		Hash h = Hash.wrap(b,pos);
		if (h==null) throw new BadFormatException("Insufficient bytes to read Ref as position: "+pos);
		Ref ref=Ref.forHash(h);
		ref=ref.markEmbedded(false);
		return ref;
	}

	public void validate() throws InvalidDataException {
		if (hash != null) hash.validate();
		// TODO should be using a stack for validation
		if (getStatus() < VALIDATED) {
			T o = getValue();
			if (o!=null) {
				o.validate();
			}
		}
	}

	/**
	 * Return true if this Ref is a direct reference, i.e. the value is pinned in
	 * memory and cannot be garbage collected
	 * 
	 * @return true if this Ref is direct, false otherwise
	 */
	public abstract boolean isDirect();

	/**
	 * Return true if this Ref's status indicates it has definitely been persisted
	 * to storage.
	 * 
	 * May return false negatives, e.g. the object could be in the store but this
	 * Ref instance still has a status of "UNKNOWN".
	 * 
	 * @return true if this Ref has a status of PERSISTED or above, false otherwise
	 */
	public boolean isPersisted() {
		return getStatus() >= PERSISTED;
	}
	
	/**
	 * Return true if this Ref's status indicates it has definitely been marked within storage
	 * 
	 * May return false negatives, e.g. the object could be marked in the store but this
	 * Ref instance still has a status of "UNKNOWN".
	 * 
	 * @return true if this Ref has a status of MARKED or above, false otherwise
	 */
	public boolean isMarked() {
		return getStatus() >= MARKED;
	}
	
	
	/**
	 * Persists this Ref in the current store if not embedded and not already
	 * persisted.
	 * 
	 * This may convert the Ref from a direct reference to a soft reference.
	 * 
	 * If the persisted Ref represents novelty, will trigger the specified novelty
	 * handler 
	 * 
	 * @param noveltyHandler Novelty handler to call (may be null)
	 * @return the persisted Ref 
	 * @throws IOException in case of IO error during persistence
	 * @throws MissingDataException If the Ref's value does not exist or has been
	 *         garbage collected before being persisted 
	 */
	@SuppressWarnings("unchecked")
	public  Ref persist(Consumer> noveltyHandler) throws IOException {
		int status = getStatus();
		if (status >= PERSISTED) return (Ref) this; // already persisted in some form
		AStore store=Stores.current();
		return (Ref) store.storeRef(this, Ref.PERSISTED,noveltyHandler);
	}
	
	/**
	 * Persists this Ref in the current store if not embedded and not already
	 * persisted. Resulting status will be PERSISTED or higher.
	 * 
	 * This may convert the Ref from a direct reference to a soft reference.
	 * 
	 * @throws MissingDataException if the Ref cannot be fully persisted.
	 * @return the persisted Ref
	 * @throws IOException in case of IO error during persistence
	 */
	public  Ref persist() throws IOException {
		return persist(null);
	}

	/**
	 * Updates an array of Refs with the given function.
	 * 
	 * Returns the original array unchanged if no refs were changed, otherwise
	 * returns a new array.
	 * 
	 * @param refs Array of Refs to update
	 * @param func Ref update function
	 * @return Array of updated Refs
	 */
	public static  Ref[] updateRefs(Ref[] refs, IRefFunction func) {
		Ref[] newRefs = refs;
		int n = refs.length;
		for (int i = 0; i < n; i++) {
			Ref ref = refs[i];
			@SuppressWarnings("unchecked")
			Ref newRef = (Ref) func.apply(ref);
			if (ref != newRef) {
				// Ensure newRefs is a new copy since we are making at least one change
				if (newRefs == refs) {
					newRefs = refs.clone();
				}
				newRefs[i] = newRef;
			}
		}
		return newRefs;
	}

	@SuppressWarnings("unchecked")
	public static  Ref[] createArray(T[] values) {
		int n = values.length;
		Ref[] refs = new Ref[n];
		for (int i = 0; i < n; i++) {
			refs[i] = Ref.get(values[i]);
		}
		return refs;
	}

	/**
	 * Adds the value of this Ref and all non-embedded child values to a given set.
	 * 
	 * Logically, provides the guarantee that the set will contain all cells needed
	 * to recreate the complete value of this Ref.
	 * 
	 * @param store Store to add to
	 * @return Set containing this Ref and all direct or indirect child refs
	 */
	@SuppressWarnings("unchecked")
	public ASet addAllToSet(ASet store) {
		store = store.includeRef((Ref) this);
		ACell rc = getValue();
		
		int n = rc.getRefCount();
		for (int i = 0; i < n; i++) {
			Ref rr = rc.getRef(i);
			if (rr.isEmbedded()) continue;
			store = rr.addAllToSet(store);
		}
		return store;
	}

	/**
	 * Check if the Ref's value is embedded. 
	 * 
	 * If false, the value must be a branch.
	 * 
	 * @return true if embedded, false otherwise
	 */
	public final boolean isEmbedded() {
		if ((flags&KNOWN_EMBEDDED_MASK)!=0) return true; 
		if ((flags&NON_EMBEDDED_MASK)!=0) return false;
		boolean em;
		ACell value=getValue();
		if (value==null) {
			em=true;
		} else {
			em=value.isEmbedded();
		}
		flags=flags|(em?KNOWN_EMBEDDED_MASK:NON_EMBEDDED_MASK);
		return em;
	}

	/**
	 * Converts this Ref to a RefDirect
	 * @return Direct Ref
	 */
	public abstract RefDirect toDirect();
	
	/**
	 * Converts this Ref to a RefSoft. Does not perform any persistence: doing this
	 * may make MissingDataExceptions happen.
	 * @param store store to set
	 * 
	 * @return Direct Ref
	 */
	public abstract RefSoft toSoft(AStore store);

	/**
	 * Persists a Ref shallowly in the current store.
	 * 
	 * Status will be updated to STORED or higher.
	 * 
	 * @return Ref with status of STORED or above
	 * @throws IOException in case of IO error during persistence
	 */
	public  Ref persistShallow() throws IOException {
		return persistShallow(null);
	}
	
	/**
	 * Persists a Ref shallowly in the current store.
	 * 
	 * Status will be updated STORED or higher. Novelty handler will be called exactly once if and only if
	 * the ref was not previously stored
	 * 
	 * @param noveltyHandler Novelty handler to call (may be null)
	 * @return Ref with status of STORED or above
	 * @throws IOException in case of IO error during persistence
	 */
	@SuppressWarnings("unchecked")
	public  Ref persistShallow(Consumer> noveltyHandler) throws IOException {
		AStore store=Stores.current();
		return (Ref) store.storeTopRef((Ref)this, Ref.STORED, noveltyHandler);
	}

	/**
	 * Updates the value stored within this Ref. New value must be equal in value to the old value 
	 * (identical hash), but may have updated internal refs etc.
	 * 
	 * @param newValue New value
	 * @return Updated Ref. May be identical if value unchanged
	 */
	public abstract Ref withValue(T newValue);
	
	/**
	 * Writes the ref to a byte array. Embeds embedded values as necessary.
	 * @param bs Byte array to encode to
	 * @return Updated position
	 */
	@Override
	public final int encode(byte[] bs, int pos) {
		if (isEmbedded()) {
			T value=getValue();
			return Format.write(bs, pos,value); // handles null and re-uses existing encodings
		} else {
			bs[pos++]=Tag.REF;
			return getHash().getBytes(bs, pos);
		}
	}
	
	@Override
	protected Blob createEncoding() {
		if (isEmbedded()) {
			return Format.encodedBlob(getValue());
		}
		
		byte[] bs=new byte[Ref.INDIRECT_ENCODING_LENGTH];	
		Hash h=getHash();
		int pos=0;
		bs[pos++]=Tag.REF;
		pos=h.getBytes(bs, pos);
		return Blob.wrap(bs,0,pos);
	}

	/**
	 * Gets the encoding length for writing this Ref. Will be equal to the encoding length
	 * of the Ref's value if embedded, otherwise INDIRECT_ENCODING_LENGTH
	 *  
	 * @return Exact length of encoding
	 */
	public final long getEncodingLength() {
		if (isEmbedded()) {
			T value=getValue();
			if (value==null) return 1;
			return value.getEncodingLength();
		} else {
			return Ref.INDIRECT_ENCODING_LENGTH;
		}
	}

	/**
	 * Gets the indirect memory size for this Ref
	 * @return 0 for fully embedded values with no child refs, memory size of referred value otherwise
	 */
	public long getMemorySize() {
		T value=getValue();
		if (value==null) return 0;
		return value.getMemorySize();
	}

	/**
	 * Finds all instances of missing data in this Ref, and adds them to the missing set
	 * @param missingSet Set to add missing instances to
	 * @param limit Maximum number of missing branches to identity
	 */
	public void findMissing(HashSet missingSet, long limit) {
		if (getStatus()>=Ref.PERSISTED) return;

		final ArrayList> stack=new ArrayList<>();
		stack.add(this);
		Consumer> mf=r->{
			if (missingSet.size()>=limit) return;
			if (missingSet.contains(r.cachedHash())) return;
			if (r.isMissing()) {
				missingSet.add(r.cachedHash());
			} else {
				if (r.getStatus()>=Ref.PERSISTED) return; // proof we have everything below here
					
				// Should be OK to get value, since non-missing!
				ACell val=r.getValue();
				if (val==null) return;
				
				// recursively scan for missing children
				int n=val.getRefCount();
				for (int i=0; i ensureCanonical();

	/**
	 * Updates Refs in an arbitrary Cell
	 * @param  Type of Cell
	 * @param o Cell to update
	 * @param func Ref update function
	 * @return Updated Cell (will be the same cell if Refs unchanged)
	 */
	@SuppressWarnings("unchecked")
	public static  T update(T o, IRefFunction func) {
		if (o==null) return null;
		return (T) o.updateRefs(func);
	}

	@SuppressWarnings("unchecked")
	public static  R updateRefs(R a, IRefFunction func) {
		if (a==null) return null;
		R b=(R) a.updateRefs(func);
		return b;
	}

	@SuppressWarnings("unchecked")
	public static final  Ref nil() {
		return (Ref)NULL_VALUE;
	}

	public boolean isInternal() {
		return (flags & STATUS_MASK) == INTERNAL;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy