Skip to content

Commit bd5ac6d

Browse files
committed
feat: github collectStarsWorker and tests
1 parent c8b96e5 commit bd5ac6d

File tree

10 files changed

+260
-12
lines changed

10 files changed

+260
-12
lines changed

src/constants.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export const secretAccessKey = getConfigs().secretAccessKey;
66
export const githubAPIToken = getConfigs().githubAPIToken;
77
export const cocoEndPoint = getConfigs().cocoEndPoint;
88
export const cocoAuthKey = getConfigs().cocoAuthKey;
9+
// GitHub API rate limit is 5000 for authenticated users. Be conservative and set this to 2000.
10+
export const githubHourlyRateLimit = getConfigs().githubHourlyRateLimit;
11+
// the repository where public ops related issues can be created. Eg. "phcode-dev/extensionService"
12+
export const opsRepo = getConfigs().opsRepo;
913
export const DATABASE_NAME = `phcode_extensions_${stage}`;
1014
export const EXTENSIONS_DETAILS_TABLE = `${DATABASE_NAME}.extensionDetails`;
1115
export const RELEASE_DETAILS_TABLE = `${DATABASE_NAME}.releaseDetails`;

src/github.js

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,49 @@
11
import { Octokit } from "@octokit/rest";
2-
import {githubAPIToken} from "./constants.js";
2+
import {githubAPIToken, githubHourlyRateLimit, opsRepo, stage} from "./constants.js";
33

44
// github api docs: https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#create-an-issue
55
export const _gitHub = {
66
Octokit
77
};
88

99
let octokit;
10+
const ONE_HOUR = 1000*60*60,
11+
ONE_DAY = ONE_HOUR * 24;
12+
let requestsInThisHour = 0,
13+
githubOpsIssueForTheDay, issueUpdatedInThisHour = false;
14+
15+
/* c8 ignore start */
16+
// not testing this as no time and is manually tested. If you are touching this code, manual test thoroughly
17+
function _resetTPH() {
18+
requestsInThisHour = 0;
19+
issueUpdatedInThisHour = false;
20+
}
21+
function _resetGithubOpsIssue() {
22+
githubOpsIssueForTheDay = null;
23+
}
24+
export function setupGitHubOpsMonitoring() {
25+
setInterval(_resetTPH, ONE_HOUR);
26+
setInterval(_resetGithubOpsIssue, ONE_DAY);
27+
}
28+
/* c8 ignore stop */
29+
30+
async function _newRequestMetric() {
31+
requestsInThisHour++;
32+
if(requestsInThisHour > githubHourlyRateLimit/2) {
33+
let opsRepoSplit = opsRepo.split("/"); //Eg. "phcode-dev/extensionService"
34+
if(issueUpdatedInThisHour){
35+
return;
36+
}
37+
issueUpdatedInThisHour = true;
38+
const message = `Github API requests for the hour is at ${requestsInThisHour}, Max allowed is ${githubHourlyRateLimit}`;
39+
if(!githubOpsIssueForTheDay) {
40+
githubOpsIssueForTheDay = await createIssue(opsRepoSplit[0], opsRepoSplit[1],
41+
`[OPS-${stage}] Github Rate Limit above threshold.`, message);
42+
} else {
43+
await commentOnIssue(opsRepoSplit[0], opsRepoSplit[1], githubOpsIssueForTheDay.number, message);
44+
}
45+
}
46+
}
1047

1148
export function initGitHubClient() {
1249
if(octokit){
@@ -37,6 +74,7 @@ export async function createIssue(owner, repo, title, body) {
3774
// https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#create-an-issue
3875
// {... "html_url": "https://github.com/octocat/Hello-World/issues/1347", ...}
3976
console.log("create Github issue: ", arguments);
77+
_newRequestMetric();
4078
let response = await octokit.request(`POST /repos/${owner}/${repo}/issues`, {
4179
owner,
4280
repo,
@@ -62,6 +100,7 @@ export async function createIssue(owner, repo, title, body) {
62100
export async function commentOnIssue(owner, repo, issueNumber, commentString) {
63101
// https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28
64102
console.log("Comment on Github issue: ", arguments);
103+
_newRequestMetric();
65104
let response = await octokit.request(`POST /repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
66105
owner,
67106
repo,
@@ -86,6 +125,7 @@ export async function getOrgDetails(org) {
86125
// https://docs.github.com/en/rest/orgs/orgs?apiVersion=2022-11-28#get-an-organization
87126
console.log("Get Org details: ", arguments);
88127
try{
128+
_newRequestMetric();
89129
let response = await octokit.request(`GET /orgs/${org}`, {
90130
org
91131
});
@@ -117,10 +157,11 @@ export async function getOrgDetails(org) {
117157
* @param {string} repo
118158
* @return {Promise<{html_url:string, stargazers_count:number}> | null}
119159
*/
120-
export async function getRepoDetails(owner, repo) {
160+
export async function getRepoDetails(owner, repo, log = true) {
121161
// https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#get-a-repository
122-
console.log("Get Repo details: ", arguments);
162+
log && console.log("Get Repo details: ", arguments);
123163
try{
164+
_newRequestMetric();
124165
let response = await octokit.request(`GET /repos/${owner}/${repo}`, {
125166
owner, repo
126167
});
@@ -130,7 +171,7 @@ export async function getRepoDetails(owner, repo) {
130171
stargazers_count: response.data.stargazers_count
131172
};
132173

133-
console.log("GitHub repo details: ", repoDetails);
174+
log && console.log("GitHub repo details: ", repoDetails);
134175
return repoDetails;
135176
} catch (e) {
136177
if(e.status === 404){
@@ -155,6 +196,7 @@ export async function getReleaseDetails(owner, repo, tag) {
155196
// https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#get-a-repository
156197
console.log("Get Release details: ", arguments);
157198
try{
199+
_newRequestMetric();
158200
let response = await octokit.request(`GET /repos/${owner}/${repo}/releases/tags/${tag}`, {
159201
owner, repo, tag
160202
});

src/server.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ import {getHelloSchema, hello} from "./api/hello.js";
2929
import {getPublishGithubReleaseSchema, publishGithubRelease} from "./api/publishGithubRelease.js";
3030
import path from 'path';
3131
import { fileURLToPath } from 'url';
32-
import {initGitHubClient} from "./github.js";
32+
import {initGitHubClient, setupGitHubOpsMonitoring} from "./github.js";
3333
import {getGetGithubReleaseStatusSchema, getGithubReleaseStatus} from "./api/getGithubReleaseStatus.js";
3434
import {getCountDownloadSchema, countDownload} from "./api/countDownload.js";
35+
import {startCollectStarsWorker} from "./utils/sync.js";
3536

3637
const __filename = fileURLToPath(import.meta.url);
3738
const __dirname = path.dirname(__filename);
@@ -124,6 +125,8 @@ export async function startServer() {
124125
console.log("awaiting connection to coco db");
125126
await db.init(cocoEndPoint, cocoAuthKey);
126127
initGitHubClient();
128+
setupGitHubOpsMonitoring();
129+
startCollectStarsWorker();
127130
console.log("connected to coco db");
128131
setupTasks();
129132
const configs = getConfigs();

src/utils/configs.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import * as fs from "fs";
33
let APP_CONFIG = null;
44

55
function _checkRequiredConfigs(config) {
6-
const requiredConfigVars = ["cocoEndPoint", "cocoAuthKey", "stage", "githubAPIToken", "baseURL"];
6+
const requiredConfigVars = ["cocoEndPoint", "cocoAuthKey", "stage", "githubAPIToken", "baseURL",
7+
"githubHourlyRateLimit", "opsRepo"];
78
let missingEnvVars = [];
89
for (let envName of requiredConfigVars){
910
if(!config[envName]){

src/utils/sync.js

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import {
33
EXTENSIONS_DETAILS_TABLE,
44
POPULARITY_FILE,
55
REGISTRY_FILE,
6-
REGISTRY_VERSION_FILE
6+
REGISTRY_VERSION_FILE,
7+
FIELD_EXTENSION_ID
78
} from "../constants.js";
89
import db from "../db.js";
910
import {S3} from "../s3.js";
11+
import {getRepoDetails} from "../github.js";
1012

1113
export async function syncRegistryDBToS3JSON() {
1214
console.log("syncing non synced extension data in db to s3 extension.json");
@@ -58,3 +60,92 @@ export async function syncRegistryDBToS3JSON() {
5860
}
5961
console.log("syncPending status updated in db for: ", await Promise.all(updatePromises));
6062
}
63+
64+
65+
const ONE_HOUR = 1000*60*60, HOURS_IN_DAY = 24, ONE_DAY = ONE_HOUR * HOURS_IN_DAY;
66+
let extensionsStarsCollectedToday = []; // will be reset every day, collect stars from GitHub once daily
67+
68+
async function _updateStargazerCount(owner, repo, extensionId) {
69+
let repoDetails = await getRepoDetails(owner, repo, false);
70+
if(repoDetails) {
71+
const queryObj = {};
72+
queryObj[FIELD_EXTENSION_ID] = extensionId;
73+
let registryPKGJSON = await db.getFromIndex(EXTENSIONS_DETAILS_TABLE, queryObj);
74+
if(!registryPKGJSON.isSuccess){
75+
console.error("Error getting extensionPKG details from db: " + extensionId);
76+
// dont fail, continue with next repo
77+
return;
78+
}
79+
if(registryPKGJSON.documents.length === 1){
80+
const document = registryPKGJSON.documents[0];
81+
const documentId = registryPKGJSON.documents[0].documentId;
82+
document.gihubStars = repoDetails.stargazers_count;
83+
let status = await db.update(EXTENSIONS_DETAILS_TABLE, documentId, document,
84+
`$.metadata.version='${document.metadata.version}'`);
85+
if(!status.isSuccess) {
86+
console.error("Error updating stars for extension in db: " + extensionId);
87+
// dont fail, continue with next repo
88+
return;
89+
}
90+
}
91+
}
92+
}
93+
94+
/**
95+
* Collects github star count every hour in batches considering GitHub throttles at 2000 GitHub Api requests per hour.
96+
*/
97+
export async function _collectStarsWorker() { // exported for tests only
98+
console.log("Number of extensions whose stars collected today: ", extensionsStarsCollectedToday.length);
99+
let registry = JSON.parse(await S3.getObject(EXTENSIONS_BUCKET, REGISTRY_FILE));
100+
let extensionIDs = Object.keys(registry);
101+
const numExtensionsToCollect = (extensionIDs.length/HOURS_IN_DAY) * 2; // so that the task completes in half day
102+
let extensionsToCollect = []; // all extensions whose stars have not been collected today
103+
for(let extensionID of extensionIDs) {
104+
if(extensionsStarsCollectedToday.includes(extensionID)){
105+
// already collected
106+
continue;
107+
}
108+
if(!registry[extensionID].ownerRepo){
109+
// no repo, so nothing to see here
110+
extensionsStarsCollectedToday.push(extensionID);
111+
continue;
112+
}
113+
extensionsToCollect.push(extensionID);
114+
}
115+
let collectedStarsForExtensions = [];
116+
for(let i=0; i < numExtensionsToCollect && i < extensionsToCollect.length; i++){
117+
let extensionID = extensionsToCollect[i];
118+
let repoSplit = registry[extensionID].ownerRepo.split("/");//"https://github.com/Brackets-Themes/808"
119+
const repo = repoSplit[repoSplit.length-1],
120+
owner = repoSplit[repoSplit.length-2];
121+
// this is purposefully serial
122+
await _updateStargazerCount(owner, repo, extensionID);
123+
extensionsStarsCollectedToday.push(extensionID);
124+
collectedStarsForExtensions.push(extensionID);
125+
}
126+
console.log(`collecting stars for ${collectedStarsForExtensions.length} extensions of MAX allowed ${numExtensionsToCollect}`);
127+
return {collectedStarsForExtensions, extensionsStarsCollectedToday};
128+
}
129+
130+
/* c8 ignore start */
131+
// not testing this as no time and is manually tested. If you are touching this code, manual test thoroughly
132+
let worker;
133+
export function startCollectStarsWorker() {
134+
if(worker){
135+
return;
136+
}
137+
worker = setInterval(_collectStarsWorker, ONE_HOUR);
138+
setInterval(()=>{
139+
extensionsStarsCollectedToday = [];
140+
}, ONE_DAY);
141+
}
142+
/* c8 ignore end */
143+
/**
144+
* sets download count of extensions.
145+
* publishes registry.json and popularity.json into s3
146+
* does not increase registry_version.json
147+
* @private
148+
*/
149+
function _syncPopularityHourly() {
150+
151+
}

test/integration/hello.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ describe('Integration Tests for hello api', function () {
4747
let output = await fetch("http://localhost:5000/www", { method: 'GET'});
4848
expect(output.status).eql(200);
4949
output = await output.text();
50-
expect(output.includes("Hello HTML")).eql(true);
50+
expect(output.includes("<!DOCTYPE html>")).eql(true);
5151
output = await fetch("http://localhost:5000/www/", { method: 'GET'});
5252
expect(output.status).eql(200);
5353
output = await output.text();
54-
expect(output.includes("Hello HTML")).eql(true);
54+
expect(output.includes("<!DOCTYPE html>")).eql(true);
5555
});
5656

5757
it('should get 404 if static web page doesnt exist', async function () {

test/integration/setupTestConfig.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ const defaultTestConfig = {
1212
"accessKeyId": "update in testConfig.json file to run integ tests",
1313
"secretAccessKey": "update in testConfig.json file to run integ tests",
1414
"githubAPIToken": "update in testConfig.json file to run integ tests",
15-
"baseURL": "update in testConfig.json file to run integ tests"
15+
"baseURL": "update in testConfig.json file to run integ tests",
16+
"githubHourlyRateLimit": "update in testConfig.json file to run integ tests",
17+
"opsRepo": "update in testConfig.json file to run integ tests"
1618
};
1719

1820

test/unit/setupMocks.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,11 @@ export function setS3Mock(bucket, key, contents) {
135135
* @param org
136136
* @param repo
137137
*/
138-
export function getRepoDetails(org, repo) {
138+
export function getRepoDetails(org, repo, starGazers = 3) {
139139
mockedFunctions.githubRequestFnMock = githubRequestFnMock;
140140
getRepoDetailsResponses[`${org}/${repo}`] = {
141141
data: {
142-
stargazers_count: 3,
142+
stargazers_count: starGazers,
143143
html_url: `https://github.com/${org}/${repo}`
144144
}
145145
};

test/unit/utils/.app.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"stage": "test",
88
"githubAPIToken": "githubToken",
99
"baseURL": "http://localhost:5000",
10+
"githubHourlyRateLimit": 2000,
11+
"opsRepo": "phcode-dev/extensionService",
1012
"mysql": {
1113
"host": "localhost",
1214
"port": "3306",

0 commit comments

Comments
 (0)