@kyve/core-beta
Version:
🚀 The base KYVE node implementation.
235 lines (203 loc) • 8.8 kB
text/typescript
import seedrandom from "seedrandom";
import { DataItem, Node } from "../..";
import { generateIndexPairs, sleep, standardizeJSON } from "../../utils";
/**
* runCache is the other main execution thread for collecting data items
* which will get packed into bundles and submitted to the network
* in order to archive them. This method should run indefinitely.
*
* This method stays in sync with the bundle proposal rounds
* where the other main method "runNode" takes part. It works
* by running in parallel to the validation and submission of
* bundle proposals. When data needs to be validated or proposed
* the other method simply looks in the globally available cache
* and checks if this method already added some items into it.
*
* It starts by getting the current pool index and checking at
* from which index to which the node has to collect the data items
* in order to participate in the current proposal round.
*
* After a bundle proposal got finalized the cache gets cleared of
* all finalized data items since they are not needed anymore and
* starts collecting the data items which are needed for the
* following round.
*
* @method runCache
* @param {Node} this
* @return {Promise<void>}
*/
export async function runCache(this: Node): Promise<void> {
// run rounds indefinitely, continueRound returns always
// true and is only used by unit tests to control the termination of
// rounds by mocking it
while (this.continueRound()) {
try {
// if there is no storage id we can assume that the last
// bundle has been dropped or invalidated. In that case we
// reset the cache
if (!this.pool.bundle_proposal!.storage_id) {
this.logger.debug(`this.cacheProvider.drop()`);
await this.cacheProvider.drop();
this.m.cache_current_items.set(0);
}
// determine the creation time of the current bundle proposal
// if the creation time ever increases this means a new bundle
// proposal is available
const updatedAt = parseInt(this.pool.bundle_proposal!.updated_at);
// determine the current index of the pool. All data items
// before the current index can be deleted since they are already
// finalized. Data items should always be cached from this index
// and not before
const currentIndex = parseInt(this.pool.data!.current_index);
// determine the target index. Here the target index is the
// index the cache should collect data in this particular round.
// We start from the current index and first index all the way
// to the current bundle proposal. Since the next uploader
// creates a bundle starting from the current bundle proposal
// we further index to the maximum possible bundle size ahead
const targetIndex =
currentIndex +
parseInt(this.pool.bundle_proposal!.bundle_size) +
parseInt(this.pool.data!.max_bundle_size);
// delete all data items which came before the current index
// because they got finalized and are not needed anymore
this.logger.debug(
`Deleting data from index ${Math.max(
0,
currentIndex - parseInt(this.pool.data!.max_bundle_size)
)} to index ${currentIndex}`
);
for (
let i = Math.max(
0,
currentIndex - parseInt(this.pool.data!.max_bundle_size)
);
i < currentIndex;
i++
) {
try {
this.logger.debug(`this.cacheProvider.del(${i.toString()})`);
await this.cacheProvider.del(i.toString());
this.m.cache_current_items.dec();
} catch {
continue;
}
}
this.m.cache_index_tail.set(Math.max(0, currentIndex - 1));
// determine the start key for the current caching round
// this key gets increased overtime to temp save the
// current key while collecting the data items
let key = this.pool.data!.current_key;
// collect all data items from current pool index to
// the target index
this.logger.debug(
`Caching from index ${currentIndex} to index ${targetIndex}`
);
for (let i = currentIndex; i < targetIndex; i++) {
// check if data item was already collected. If it was
// already collected we don't need to retrieve it again
this.logger.debug(`this.cacheProvider.exists(${i.toString()})`);
const itemFound = await this.cacheProvider.exists(i.toString());
// retrieve the next key from the deterministic runtime
// specific implementation. If the start key is not defined
// the pool is in genesis state and therefore the pool
// specific start key should be used
if (key) {
this.logger.debug(`this.runtime.nextKey(${key})`);
}
const nextKey = key
? await this.runtime.nextKey(key)
: this.pool.data!.start_key;
if (!itemFound) {
// collect and transform data from every source at once
const results: DataItem[] = await Promise.all(
this.poolConfig.sources.map((source: string) =>
this.saveGetTransformDataItem(source, nextKey)
)
);
// validate if data items from those multiple sources are
// valid against each other
let valid = true;
// we generate all possible index pairs so we can cross-validate
// each data item with every other data item to ensure that
// everything is correct
const indexPairs = generateIndexPairs(results.length);
// validate every data item for each possible index pair
for (const pair of indexPairs) {
try {
// validate pair of data items
valid = await this.runtime.validateDataItem(
this,
results[pair[0]],
results[pair[1]]
);
// if an invalid data item pair was found abort and don't save
// to cache
if (!valid) {
this.logger.info(
`Found mismatching data item between sources ${
this.poolConfig.sources[pair[0]]
} and ${this.poolConfig.sources[pair[1]]}`
);
break;
}
} catch (err) {
this.logger.error(
`Unexpected error validating data items between sources ${
this.poolConfig.sources[pair[0]]
} and ${this.poolConfig.sources[pair[1]]}`
);
this.logger.error(standardizeJSON(err));
// if data item validation fails abort and don't save to cache
valid = false;
break;
}
}
// if validation between sources fails we abort further data collection
if (!valid) {
break;
}
// a random item from the result gets chosen. seed is the current item key
const seed = i.toString();
// calculate randIndex in results range
const randIndex = Math.floor(
seedrandom(seed).quick() * results.length
);
this.logger.debug(
`Choosing item from seed:${seed} index:${randIndex} source:${this.poolConfig.sources[randIndex]}`
);
// add this data item to the cache
this.logger.debug(`this.cacheProvider.put(${i.toString()},$ITEM)`);
await this.cacheProvider.put(i.toString(), results[randIndex]);
this.m.cache_current_items.inc();
this.m.cache_index_head.set(i);
// add a timeout so that the runtime data source
// is not overloaded with requests
await sleep(50);
}
// assign the next key for the next round
key = nextKey;
}
// wait until a new bundle proposal is available. We don't need
// to sync the pool here because the pool state already gets
// synced in the other main function "runNode" so we only listen
await this.waitForCacheContinuation(updatedAt);
} catch (err) {
this.logger.error(
`Unexpected error collecting data items to local cache. Continuing ...`
);
this.logger.error(standardizeJSON(err));
try {
// drop cache if an unexpected error occurs during caching
this.logger.debug(`this.cacheProvider.drop()`);
await this.cacheProvider.drop();
this.m.cache_current_items.set(0);
} catch (dropError) {
this.logger.error(
`Unexpected error dropping local cache. Continuing ...`
);
this.logger.error(standardizeJSON(dropError));
}
}
}
}