Skip to content

Commit

Permalink
Feat: Add blocks and tx count to slot feed (#1346)
Browse files Browse the repository at this point in the history
* feat: add Slot Analytics influx

* fix: add slot stats to landing page + update useEpochStats + fix burnedManaCache

* fix: display 0 if stat undefined

* fix: burned mana cell

* fix: change no_payload_count to validation_count

* hotfix: slots feed links not absolute

---------

Co-authored-by: Begoña Alvarez <[email protected]>
  • Loading branch information
brancoder and begonaalvarezd authored Mar 28, 2024
1 parent 2b2b647 commit 2c4836c
Show file tree
Hide file tree
Showing 16 changed files with 332 additions and 29 deletions.
11 changes: 11 additions & 0 deletions api/src/models/api/nova/stats/slot/ISlotAnalyticStatsRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface ISlotAnalyticStatsRequest {
/**
* The network to search on.
*/
network: string;

/**
* The slot index to get the stats for.
*/
slotIndex: string;
}
12 changes: 12 additions & 0 deletions api/src/models/api/nova/stats/slot/ISlotAnalyticStatsResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IResponse } from "../../IResponse";

export interface ISlotAnalyticStatsResponse extends IResponse {
slotIndex?: number;
blockCount?: number;
perPayloadType?: {
transaction?: number;
candidacy?: number;
taggedData?: number;
noPayload?: number;
};
}
16 changes: 16 additions & 0 deletions api/src/models/influx/nova/IInfluxDbCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ interface IEpochAnalyticStats {
};
}

interface ISlotAnalyticStats {
slotIndex: number;
blockCount: number;
perPayloadType: {
transaction: number;
taggedData: number;
candidacy: number;
noPayload: number;
};
}

/**
* The epoch stats cache. Map epoch index to stats.
*/
Expand All @@ -89,6 +100,11 @@ export type ManaBurnedInSlot = ITimedEntry & {
*/
export type ManaBurnedInSlotCache = Map<number, ManaBurnedInSlot>;

/**
* The slot stats cache. Map slot index to stats.
*/
export type IInfluxSlotAnalyticsCache = Map<number, ISlotAnalyticStats>;

/**
* The helper to initialize empty maps
* @returns The initial cache object
Expand Down
6 changes: 6 additions & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,4 +336,10 @@ export const routes: IRoute[] = [
folder: "nova/epoch/influx",
func: "get",
},
{
path: "/nova/slot/stats/:network/:slotIndex",
method: "get",
folder: "nova/slot/blocks/stats",
func: "get",
},
];
6 changes: 1 addition & 5 deletions api/src/routes/nova/epoch/influx/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { IConfiguration } from "../../../../models/configuration/IConfiguration"
import { NOVA } from "../../../../models/db/protocolVersion";
import { NetworkService } from "../../../../services/networkService";
import { InfluxServiceNova } from "../../../../services/nova/influx/influxServiceNova";
import { NodeInfoService } from "../../../../services/nova/nodeInfoService";
import { ValidationHelper } from "../../../../utils/validationHelper";

/**
Expand All @@ -33,10 +32,7 @@ export async function get(_: IConfiguration, request: IEpochAnalyticStatsRequest
}

const influxService = ServiceFactory.get<InfluxServiceNova>(`influxdb-${request.network}`);
const nodeService = ServiceFactory.get<NodeInfoService>(`node-info-${request.network}`);
const protocolParameters = await nodeService.getProtocolParameters();

if (!influxService || !protocolParameters) {
if (!influxService) {
return { error: "Influx service not found for this network." };
}

Expand Down
42 changes: 42 additions & 0 deletions api/src/routes/nova/slot/blocks/stats/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ServiceFactory } from "../../../../../factories/serviceFactory";
import { ISlotAnalyticStatsRequest } from "../../../../../models/api/nova/stats/slot/ISlotAnalyticStatsRequest";
import { ISlotAnalyticStatsResponse } from "../../../../../models/api/nova/stats/slot/ISlotAnalyticStatsResponse";
import { IConfiguration } from "../../../../../models/configuration/IConfiguration";
import { NOVA } from "../../../../../models/db/protocolVersion";
import { NetworkService } from "../../../../../services/networkService";
import { InfluxServiceNova } from "../../../../../services/nova/influx/influxServiceNova";
import { ValidationHelper } from "../../../../../utils/validationHelper";

/**
* Fetch the slot stats from influx nova.
* @param _ The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(_: IConfiguration, request: ISlotAnalyticStatsRequest): Promise<ISlotAnalyticStatsResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");
ValidationHelper.numberFromString(request.slotIndex, "slotIndex");

const networkConfig = networkService.get(request.network);

if (networkConfig.protocolVersion !== NOVA) {
return {};
}

const influxService = ServiceFactory.get<InfluxServiceNova>(`influxdb-${networkConfig.network}`);

if (influxService) {
const slotIndex = Number.parseInt(request.slotIndex, 10);
const slotStats = await influxService.getSlotAnalyticStats(slotIndex);

if (slotStats) {
return slotStats;
}

return { error: `Could not fetch stats for slot ${request.slotIndex}` };
}

return { error: "Influx service not found for this network." };
}
4 changes: 2 additions & 2 deletions api/src/services/nova/influx/influxQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ export const OUTPUTS_DAILY_QUERY = {
`,
};

export const EPOCH_STATS_QUERY_BY_EPOCH_INDEX = `
export const BLOCK_STATS_QUERY = `
SELECT
sum("transaction_count") AS "transaction",
sum("tagged_data_count") AS "taggedData",
sum("candidacy_announcement_count") AS "candidacy",
sum("no_payload_count") AS "noPayload"
sum("validation_count") AS "noPayload"
FROM "iota_block_activity"
WHERE time >= $from and time <= $to
`;
Expand Down
126 changes: 120 additions & 6 deletions api/src/services/nova/influx/influxServiceNova.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import {
TRANSACTION_DAILY_QUERY,
UNLOCK_CONDITIONS_PER_TYPE_DAILY_QUERY,
VALIDATORS_ACTIVITY_DAILY_QUERY,
EPOCH_STATS_QUERY_BY_EPOCH_INDEX,
DELEGATORS_TOTAL_QUERY,
BLOCK_STATS_QUERY,
} from "./influxQueries";
import { ServiceFactory } from "../../../factories/serviceFactory";
import logger from "../../../logger";
Expand All @@ -39,6 +39,7 @@ import {
IInfluxAnalyticsCache,
IInfluxDailyCache,
IInfluxEpochAnalyticsCache,
IInfluxSlotAnalyticsCache,
initializeEmptyDailyCache,
ManaBurnedInSlot,
ManaBurnedInSlotCache,
Expand Down Expand Up @@ -79,12 +80,25 @@ type EpochUpdate = ITimedEntry & {
noPayload: number;
};

type SlotUpdate = ITimedEntry & {
slotIndex: number;
taggedData: number;
candidacy: number;
transaction: number;
noPayload: number;
};

/**
* Epoch analyitics cache MAX size.
*/
const EPOCH_CACHE_MAX = 20;

/**
* Slot analyitics cache MAX size.
*/
const SLOT_CACHE_MAX = 200;

/*
* Epoch analyitics cache MAX size.
*/
const MANA_BURNED_CACHE_MAX = 20;
Expand All @@ -107,6 +121,12 @@ const COLLECT_ANALYTICS_DATA_CRON = "55 58 * * * *";
*/
const COLLECT_EPOCH_ANALYTICS_DATA_CRON = "*/10 * * * *";

/**
* The collect slot analytics data interval cron expression.
* Every 10 seconds
*/
const COLLECT_SLOT_ANALYTICS_DATA_CRON = "*/10 * * * * *";

export class InfluxServiceNova extends InfluxDbClient {
/**
* The InfluxDb Client.
Expand All @@ -123,6 +143,11 @@ export class InfluxServiceNova extends InfluxDbClient {
*/
protected readonly _epochCache: IInfluxEpochAnalyticsCache;

/**
* The current influx slot analytics cache instance.
*/
protected readonly _slotCache: IInfluxSlotAnalyticsCache;

/**
* The current influx analytics cache instance.
*/
Expand All @@ -148,6 +173,7 @@ export class InfluxServiceNova extends InfluxDbClient {
this._novatimeService = ServiceFactory.get<NovaTimeService>(`nova-time-${network.network}`);
this._dailyCache = initializeEmptyDailyCache();
this._epochCache = new Map();
this._slotCache = new Map();
this._manaBurnedInSlotCache = new Map();
this._analyticsCache = {};
}
Expand Down Expand Up @@ -268,6 +294,14 @@ export class InfluxServiceNova extends InfluxDbClient {
return this._epochCache.get(epochIndex);
}

public async getSlotAnalyticStats(slotIndex: number) {
if (!this._slotCache.get(slotIndex)) {
await this.collectSlotStatsByIndex(slotIndex);
}

return this._slotCache.get(slotIndex);
}

/**
* Get the manaBurned stats by slot index.
* @param slotIndex - The slot index.
Expand Down Expand Up @@ -311,6 +345,8 @@ export class InfluxServiceNova extends InfluxDbClient {
void this.collectAnalytics();
// eslint-disable-next-line no-void
void this.collectEpochStats();
// eslint-disable-next-line no-void
void this.collectSlotStats();

if (this._client) {
cron.schedule(COLLECT_GRAPHS_DATA_CRON, async () => {
Expand All @@ -327,6 +363,11 @@ export class InfluxServiceNova extends InfluxDbClient {
// eslint-disable-next-line no-void
void this.collectEpochStats();
});

cron.schedule(COLLECT_SLOT_ANALYTICS_DATA_CRON, async () => {
// eslint-disable-next-line no-void
void this.collectSlotStats();
});
}
}

Expand Down Expand Up @@ -485,17 +526,15 @@ export class InfluxServiceNova extends InfluxDbClient {
const fromNano = toNanoDate((moment(Number(from) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString());
const toNano = toNanoDate((moment(Number(to) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString());

await this.queryInflux<EpochUpdate>(EPOCH_STATS_QUERY_BY_EPOCH_INDEX, fromNano, toNano)
await this.queryInflux<EpochUpdate>(BLOCK_STATS_QUERY, fromNano, toNano)
.then((results) => {
for (const update of results) {
update.epochIndex = epochIndex;
this.updateEpochCache(update);
}
})
.catch((e) => {
logger.warn(
`[InfluxClient] Query ${EPOCH_STATS_QUERY_BY_EPOCH_INDEX} failed for (${this._network.network}). Cause ${e}`,
);
logger.warn(`[InfluxClient] Query ${BLOCK_STATS_QUERY} failed for (${this._network.network}). Cause ${e}`);
});
} catch (err) {
logger.warn(`[InfluxNova] Failed refreshing epoch stats for "${this._network.network}". Cause: ${err}`);
Expand Down Expand Up @@ -574,7 +613,82 @@ export class InfluxServiceNova extends InfluxDbClient {

logger.debug(`[InfluxNova] Deleting epoch index "${lowestIndex}" ("${this._network.network}")`);

this._epochCache.delete(lowestIndex);
this._manaBurnedInSlotCache.delete(lowestIndex);
}
}
}

/**
* Get the slot analytics by index and set it in the cache.
* @param slotIndex - The slot index.
*/
private async collectSlotStatsByIndex(slotIndex: number) {
try {
const { from, to } = this._novatimeService.getSlotIndexToUnixTimeRange(slotIndex);
const fromNano = toNanoDate((moment(Number(from) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString());
const toNano = toNanoDate((moment(Number(to) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString());

await this.queryInflux<SlotUpdate>(BLOCK_STATS_QUERY, fromNano, toNano)
.then((results) => {
for (const update of results) {
update.slotIndex = slotIndex;
this.updateSlotCache(update);
}
})
.catch((e) => {
logger.warn(`[InfluxClient] Query ${BLOCK_STATS_QUERY} failed for (${this._network.network}). Cause ${e}`);
});
} catch (err) {
logger.warn(`[InfluxNova] Failed refreshing slot stats for "${this._network.network}". Cause: ${err}`);
}
}

/**
* Get the slot analytics and set it in the cache.
*/
private async collectSlotStats() {
try {
logger.debug(`[InfluxNova] Collecting slot stats for "${this._network.network}"`);
const slotIndex = this._novatimeService.getUnixTimestampToSlotIndex(moment().unix());
// eslint-disable-next-line no-void
void this.collectSlotStatsByIndex(slotIndex);
} catch (err) {
logger.warn(`[InfluxNova] Failed refreshing slot stats for "${this._network.network}". Cause: ${err}`);
}
}

private updateSlotCache(update: SlotUpdate) {
if (update.slotIndex !== undefined && !this._slotCache.has(update.slotIndex)) {
const { slotIndex, transaction, candidacy, taggedData, noPayload } = update;
const blockCount = transaction + candidacy + taggedData + noPayload;
this._slotCache.set(slotIndex, {
slotIndex,
blockCount,
perPayloadType: {
transaction,
candidacy,
taggedData,
noPayload,
},
});

logger.debug(`[InfluxNova] Added slot index "${slotIndex}" to cache for "${this._network.network}"`);

if (this._slotCache.size > SLOT_CACHE_MAX) {
let lowestIndex: number;
for (const index of this._slotCache.keys()) {
if (!lowestIndex) {
lowestIndex = index;
}

if (slotIndex < lowestIndex) {
lowestIndex = index;
}
}

logger.debug(`[InfluxNova] Deleting slot index "${lowestIndex}" ("${this._network.network}")`);

this._slotCache.delete(lowestIndex);
}
}
}
Expand Down
Loading

0 comments on commit 2c4836c

Please sign in to comment.