Skip to content

Commit

Permalink
Merge pull request #500 from helium/feat/cloudflare
Browse files Browse the repository at this point in the history
Cloudflare
  • Loading branch information
deasydoesit authored Dec 5, 2023
2 parents 95ea293 + 0985f98 commit 9c3fbe0
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 109 deletions.
12 changes: 6 additions & 6 deletions packages/entity-invalidator/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
export const LOOKBACK_HOURS = process.env.LOOKBACK_HOURS
? Number(process.env.LOOKBACK_HOURS)
: 25;
export const INVALIDATE_ALL_RECORD_THRESHOLD = process.env.INVALIDATE_ALL_RECORD_THRESHOLD
? Number(process.env.INVALIDATE_ALL_RECORD_THRESHOLD)
: 500; // $0.005/invalidation * 500 records * 3 invalidations/record = $7.5
export const AWS_REGION = process.env.AWS_REGION;
export const CLOUDFRONT_DISTRIBUTION = process.env.CLOUDFRONT_DISTRIBUTION!;

export const DOMAIN = process.env.DOMAIN;

export const CLOUDFLARE_API_TOKEN = process.env.CLOUDFLARE_API_TOKEN;
export const CLOUDFLARE_ZONE_ID = process.env.CLOUDFLARE_ZONE_ID;

// Check for required environment variables
const requiredEnvVars = ["AWS_REGION", "CLOUDFRONT_DISTRIBUTION"];
const requiredEnvVars = ["DOMAIN", "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ZONE_ID"];
for (const varName of requiredEnvVars) {
if (!process.env[varName]) {
throw new Error(`Environment variable ${varName} is required`);
Expand Down
136 changes: 47 additions & 89 deletions packages/entity-invalidator/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
import { decodeEntityKey } from "@helium/helium-entity-manager-sdk";
import AWS from "aws-sdk";
import { Op } from "sequelize";
import { v4 as uuidv4 } from "uuid";
import { IotHotspotInfo, KeyToAsset, MobileHotspotInfo } from "./model";
// @ts-ignore
import {
AWS_REGION,
CLOUDFRONT_DISTRIBUTION,
CLOUDFLARE_API_TOKEN,
CLOUDFLARE_ZONE_ID,
LOOKBACK_HOURS,
INVALIDATE_ALL_RECORD_THRESHOLD,
DOMAIN,
} from "./env";
import { chunks } from "@helium/spl-utils";

// How long to wait to check invalidation status again
const INVALIDATION_WAIT = 10000;
// 30 minutes
const INVALIDATION_WAIT_LIMIT = 30 * 60 * 1000;
// How long to wait to perform next invalidation
const INVALIDATION_WAIT = 1000;
// Cloudflare can only invalidate 30k records per day, so
// 10k records here since we make 3 invalidations per record
const INVALIDATE_ALL_RECORD_THRESHOLD = 10000;
// Headers used across all Cloudflare invalidation requests
const CLOUDFLARE_HEADERS = {
'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,
'Content-Type': 'application/json',
};
// URL used across all Cloudflare invalidation requests
const CLOUDFLARE_API_URL = `https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache`;


async function run() {
const date = new Date();
date.setHours(date.getHours() - LOOKBACK_HOURS);
AWS.config.update({ region: AWS_REGION });
const cloudfront = new AWS.CloudFront();

// Invalidate metadata-service routes:
// - /v2/hotspot/:keyToAssetKey
Expand Down Expand Up @@ -71,14 +76,11 @@ async function run() {
console.log(`Found ${totalCount} updated records`);

if (totalCount >= INVALIDATE_ALL_RECORD_THRESHOLD) {
await invalidateAndWait({
cloudfront,
DistributionId: CLOUDFRONT_DISTRIBUTION,
Paths: {
Quantity: 1,
Items: ["/*"],
},
});
const arg = {
purge_everything: true
};

await invalidate(arg);

return;
}
Expand Down Expand Up @@ -125,20 +127,18 @@ async function run() {
}
} while (entities.length === limit);

// Split the paths into batches of 3000
const batches = chunks(paths, 3000)
// Split the paths into batches of 30
const batches = chunks(paths, 30)

// Process each batch of invalidations
let i = 0;
for (const batch of batches) {
await invalidateAndWait({
cloudfront,
DistributionId: CLOUDFRONT_DISTRIBUTION,
Paths: {
Quantity: batch.length,
Items: batch,
},
})
const arg = {
files: batch
};

await invalidate(arg);
await delay(INVALIDATION_WAIT);

console.log(`Invalidated ${i} / ${batches.length} batches`);
i++
Expand All @@ -149,72 +149,30 @@ function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function invalidateAndWait({
cloudfront,
DistributionId,
Paths,
}: {
cloudfront: AWS.CloudFront,
DistributionId: string,
Paths: {
Quantity: number;
Items: string[]
}
}) {
const invalidationResponse = await cloudfront
.createInvalidation({
DistributionId,
InvalidationBatch: {
CallerReference: `${uuidv4()}`,
Paths,
},
})
.promise();

if (invalidationResponse?.Invalidation) {
const invalidationId = invalidationResponse.Invalidation.Id;

// Check the status of the invalidation batch periodically
let invalidationStatus = await getInvalidationStatus(
cloudfront,
invalidationId
);
let totalWait = 0;
while (invalidationStatus !== "Completed") {
console.log("Invalidation in progress. Waiting for completion...");
totalWait += INVALIDATION_WAIT
await delay(INVALIDATION_WAIT); // Wait for 10 seconds before checking the status again
invalidationStatus = await getInvalidationStatus(
cloudfront,
invalidationId
);

if (totalWait > INVALIDATION_WAIT_LIMIT) {
throw new Error("Exceeded invalidation wait limit")
}
}
function getPaths(entity: KeyToAsset): string[] {
const v1 = `${DOMAIN}/v1/${entity.address}`;
const v2 = `${DOMAIN}/v2/hotspot/${entity.address}`;
if ((entity.entityKeyStr?.length || 0) >= 200) {
return [v1, v2];
}

return [`${DOMAIN}/${entity.entityKeyStr!}`, v1, v2];
}

async function getInvalidationStatus(cloudfront: AWS.CloudFront, invalidationId: string) {
const invalidationResponse = await cloudfront
.getInvalidation({
DistributionId: CLOUDFRONT_DISTRIBUTION,
Id: invalidationId,
})
.promise();
async function invalidate(arg: any): Promise<void> {
try {
const body = JSON.stringify(arg);

return invalidationResponse?.Invalidation?.Status;
}
const response = await fetch(CLOUDFLARE_API_URL, { method: 'POST', headers: CLOUDFLARE_HEADERS, body: body });
const data = await response.json();

function getPaths(entity: KeyToAsset): string[] {
const v1 = `/v1/${entity.address}`;
const v2 = `/v2/hotspot/${entity.address}`;
if ((entity.entityKeyStr?.length || 0) >= 200) {
return [v1, v2];
}
console.log(data);

return [`/${entity.entityKeyStr!}`, v1, v2];
return;
} catch (error) {
console.error('Error:', error);
throw error;
}
}

run().catch((e) => {
Expand Down
23 changes: 9 additions & 14 deletions packages/metadata-service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ async function getHotspotByKeyToAsset(request, reply) {

const assetJson = generateAssetJson(record, keyStr!);

// Needed to make CloudFront to cache for longer than 1 day
reply.header("Cache-Control", "max-age=31536000"); // 1 year in seconds
// Needed to make Cloudflare to cache for longer than 1 day
reply.header("Cloudflare-CDN-Cache-Control", "max-age=31536000"); // 1 year in seconds

return assetJson;
};
Expand Down Expand Up @@ -238,7 +238,7 @@ server.get<{ Params: { wallet: string } }>(
})
);

reply.header("Cache-Control", "no-cache");
reply.header("Cloudflare-CDN-Cache-Control", "no-cache");

return {
hotspots_count: assetJsons.length,
Expand Down Expand Up @@ -277,7 +277,7 @@ server.get<{ Querystring: { subnetwork: string } }>(
totalPages: Math.ceil(count / PAGE_SIZE),
};

reply.header("Cache-Control", "no-cache");
reply.header("Cloudflare-CDN-Cache-Control", "no-cache");

return result;
}
Expand Down Expand Up @@ -360,18 +360,13 @@ server.get<{ Querystring: { subnetwork: string; cursor?: string; } }>(
result.items = ktas.map((kta) => {
return {
key_to_asset_key: kta.address,
is_active: kta[lastItemTable]?.is_active,
lat: kta[lastItemTable]?.lat,
long: kta[lastItemTable]?.long,
};
});

// If we're on the last page of results, tell Cloudfront not to cache
// so that origin requests are made and newly added hotspots can be
// returned
if (isLastPage) {
reply.header("Cache-Control", "no-cache");
} else {
// Needed to make CloudFront to cache for longer than 1 day
reply.header("Cache-Control", "max-age=31536000"); // 1 year in seconds
}
reply.header("Cloudflare-CDN-Cache-Control", "no-cache");

return result;
}
Expand Down Expand Up @@ -423,7 +418,7 @@ server.get<{ Params: { eccCompact: string } }>(

const assetJson = generateAssetJson(record, eccCompact);

// Needed to make CloudFront to cache for longer than 1 day
// Needed to make Cloudflare to cache for longer than 1 day
reply.header("Cache-Control", "max-age=31536000"); // 1 year in seconds

return assetJson;
Expand Down

0 comments on commit 9c3fbe0

Please sign in to comment.