Skip to content
This repository was archived by the owner on Aug 27, 2024. It is now read-only.

Commit 0523f3e

Browse files
authored
Measure inactiveness (#480)
* #429 save wip * #429 save wip * #429 save wip * #429 fix stuff * #429 add test for negative case * #429 review fixes
1 parent 49085ba commit 0523f3e

File tree

10 files changed

+671
-140
lines changed

10 files changed

+671
-140
lines changed

common/util.js

+23
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,29 @@ export function setIn(obj, path, value) {
9696
return obj
9797
}
9898

99+
function findDeepInObjImpl(obj, fn, result = []) {
100+
if (typeof obj === 'object' && obj != null) {
101+
for (const key of Object.keys(obj)) {
102+
if (fn(key)) {
103+
result.push([key, obj[key]])
104+
}
105+
findDeepInObjImpl(obj[key], fn, result)
106+
}
107+
}
108+
return result
109+
}
110+
111+
/**
112+
* Returns an array of [key, val] for all
113+
* keys in provided object where fn(key) returns true.
114+
*
115+
* @param obj object to walk
116+
* @param fn will be passed key. is expected to return true iff key/ should be talken.
117+
*/
118+
export function findDeepInObj(obj, fn) {
119+
return findDeepInObjImpl(obj, fn, [])
120+
}
121+
99122
export function encode(string, encoding = 'base64') {
100123
if (encoding !== 'base64') {
101124
throw new Error(`Encoding "${encoding}" not supported`)

config/system.yaml

+5-5
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ STATIC_DIR: ./dist/client
3636
# Database configuration
3737
# sqlite or postgres
3838
DB_DRIVER: sqlite
39-
#DB_NAME: postgres
40-
#DB_USER: postgres
41-
#DB_PASS: postgres
42-
#DB_HOST: 192.168.99.100
43-
#DB_PORT: 5432
39+
# DB_NAME: zappr
40+
# DB_USER: postgres
41+
# DB_PASS: postgres
42+
# DB_HOST: 192.168.99.100
43+
# DB_PORT: 5432
4444
SQLITE_FILE: ./zappr.sqlite
4545

4646
# Token encryption
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
const schema = 'zappr_data'
2+
const tableName = 'checks'
3+
4+
module.exports.up = function (queryInterface, Sequelize) {
5+
// delay between github and check execution
6+
const lastExecutionDelayMs = queryInterface.addColumn(
7+
{
8+
schema,
9+
tableName
10+
},
11+
'last_execution_delay_ms',
12+
{
13+
type: Sequelize.INTEGER,
14+
allowNull: true
15+
})
16+
// when check was executed
17+
const lastExecution = queryInterface.addColumn(
18+
{
19+
schema,
20+
tableName
21+
},
22+
'last_execution_ts',
23+
{
24+
type: Sequelize.DATE,
25+
allowNull: true
26+
})
27+
// how long did check execution take
28+
const lastExecutionMs = queryInterface.addColumn(
29+
{
30+
schema,
31+
tableName
32+
},
33+
'last_execution_ms',
34+
{
35+
type: Sequelize.INTEGER,
36+
allowNull: true
37+
})
38+
// was check execution successful (ie we were able to set a status)
39+
const lastExecutionSuccessful = queryInterface.addColumn(
40+
{
41+
schema,
42+
tableName
43+
},
44+
'last_execution_successful',
45+
{
46+
type: Sequelize.BOOLEAN,
47+
allowNull: true
48+
})
49+
return Promise.all([
50+
lastExecutionDelayMs,
51+
lastExecution,
52+
lastExecutionMs,
53+
lastExecutionSuccessful
54+
])
55+
}
56+
57+
module.exports.down = function (queryInterface) {
58+
return Promise.all(
59+
['last_execution_successful',
60+
'last_execution_ms',
61+
'last_execution_ts',
62+
'last_execution_delay_ms'].map(col => queryInterface.removeColumn({
63+
tableName,
64+
schema
65+
}, col)))
66+
67+
}

server/checks/Autobranch.js

+7-11
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,12 @@ export default class Autobranch extends Check {
6767
info(`${repository.full_name}: Ignore issue #${issue.number}. Action was "${action}" instead of "opened".`)
6868
return
6969
}
70-
try {
71-
const owner = repository.owner.login
72-
const repo = repository.name
73-
const {sha} = await this.github.getHead(owner, repo, repository.default_branch, token)
74-
// branch could exist already
75-
await this.github.createBranch(owner, repo, branchName, sha, token)
76-
info(`Created branch ${branchName} for ${sha} in ${repository.full_name}`)
77-
} catch (e) {
78-
// but we don't care
79-
error(`Could not create branch ${branchName} for ${sha} in ${repository.full_name}`, e)
80-
}
70+
71+
const owner = repository.owner.login
72+
const repo = repository.name
73+
const {sha} = await this.github.getHead(owner, repo, repository.default_branch, token)
74+
// branch could exist already
75+
await this.github.createBranch(owner, repo, branchName, sha, token)
76+
info(`Created branch ${branchName} for ${sha} in ${repository.full_name}`)
8177
}
8278
}

server/checks/CheckRunner.js

+72-9
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import {
99
} from './index'
1010
import { getPayloadFn } from './Check'
1111
import createAuditService from '../service/AuditServiceCreator'
12+
import { checkHandler as defaultCheckHandler } from '../handler/CheckHandler'
1213
import { githubService as defaultGithubService } from '../service/GithubService'
1314
import { pullRequestHandler as defaultPullRequestHandler } from '../handler/PullRequestHandler'
14-
import merge from 'lodash/merge'
15+
import { findDeepInObj } from '../../common/util'
1516
import { logger } from '../../common/debug'
1617
const info = logger('checkrunner', 'info')
1718
const error = logger('checkrunner', 'error')
@@ -27,12 +28,36 @@ function getTokens(dbRepo) {
2728
return (dbRepo.checks || []).reduce((agg, check) => ({[check.type]: check.token, ...agg}), {})
2829
}
2930

31+
/**
32+
* Returns when this GH event was triggered based on payload.
33+
*
34+
* Necessary because there is no top-level timestamp in the payload for every event,
35+
* we have to look into the event itself and check different properties. Not cool.
36+
* So this naive implementation looks for properties /.+?_at/ with value /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/
37+
* and parses them and returns the most current.
38+
*
39+
* This approach may now work well in 100% of the cases, but avoids that we forget to
40+
* change it in the future when we support new events.
41+
*
42+
* @param payload
43+
*/
44+
export function getTriggeredAt(payload) {
45+
const endsWithAt = /^.+?_at$/
46+
const looksLikeDate = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/
47+
return findDeepInObj(payload, key => endsWithAt.test(key))
48+
.filter(([_, val]) => looksLikeDate.test(val))
49+
.map(([_, val]) => Date.parse(val))
50+
.reduce((mostCurrent, ts) => Math.max(mostCurrent, ts), 0)
51+
}
52+
3053
export default class CheckRunner {
3154
constructor(githubService = defaultGithubService,
3255
pullRequestHandler = defaultPullRequestHandler,
56+
checkHandler = defaultCheckHandler,
3357
auditService = createAuditService()) {
3458
this.githubService = githubService
3559
this.pullRequestHandler = pullRequestHandler
60+
this.checkHandler = checkHandler
3661
this.approval = new Approval(this.githubService, this.pullRequestHandler, auditService)
3762
this.autobranch = new Autobranch(this.githubService)
3863
this.commitMessage = new CommitMessage(this.githubService)
@@ -109,40 +134,78 @@ export default class CheckRunner {
109134
}
110135

111136
async handleGithubWebhook(dbRepo, checkArgs) {
112-
const {event} = checkArgs
137+
const {event, payload, config} = checkArgs
113138
const owner = dbRepo.json.owner.login
114139
const name = dbRepo.json.name
115140
const tokens = getTokens(dbRepo)
116-
info(`${owner}/${name}: Handling Github event ${event}.${checkArgs.payload.action}`)
141+
const start = Date.now()
142+
const delay = start - getTriggeredAt(payload)
143+
info(`${owner}/${name}: Handling Github event ${event}.${payload.action}`)
117144

118145
if (PullRequestLabels.isTriggeredBy(event) && tokens[PullRequestLabels.TYPE]) {
119146
info(`${owner}/${name}: Executing check PullRequestLabels`)
120-
await this.pullRequestLabels.execute(checkArgs.config, checkArgs.payload, tokens[PullRequestLabels.TYPE])
147+
await this.checkHandler.onExecutionStart(dbRepo.id, PullRequestLabels.TYPE, delay)
148+
try {
149+
await this.pullRequestLabels.execute(config, payload, tokens[PullRequestLabels.TYPE])
150+
await this.checkHandler.onExecutionEnd(dbRepo.id, PullRequestLabels.TYPE, Date.now() - start, true)
151+
} catch (e) {
152+
await this.checkHandler.onExecutionEnd(dbRepo.id, PullRequestLabels.TYPE, Date.now() - start, false)
153+
}
121154
}
122155

123156
if (Specification.isTriggeredBy(event) && tokens[Specification.TYPE]) {
124157
info(`${owner}/${name}: Executing check Specification`)
125-
await this.specification.execute(checkArgs.config, checkArgs.payload, tokens[Specification.TYPE])
158+
await this.checkHandler.onExecutionStart(dbRepo.id, Specification.TYPE, delay)
159+
try {
160+
await this.specification.execute(config, payload, tokens[Specification.TYPE])
161+
await this.checkHandler.onExecutionEnd(dbRepo.id, Specification.TYPE, Date.now() - start, true)
162+
} catch (e) {
163+
await this.checkHandler.onExecutionEnd(dbRepo.id, Specification.TYPE, Date.now() - start, false)
164+
}
126165
}
127166

128167
if (Approval.isTriggeredBy(event) && tokens[Approval.TYPE]) {
129168
info(`${owner}/${name}: Executing check Approval`)
130-
await this.approval.execute(checkArgs.config, event, checkArgs.payload, tokens[Approval.TYPE], dbRepo.id)
169+
await this.checkHandler.onExecutionStart(dbRepo.id, Approval.TYPE, delay)
170+
try {
171+
await this.approval.execute(config, event, payload, tokens[Approval.TYPE], dbRepo.id)
172+
await this.checkHandler.onExecutionEnd(dbRepo.id, Approval.TYPE, Date.now() - start, true)
173+
} catch (e) {
174+
await this.checkHandler.onExecutionEnd(dbRepo.id, Approval.TYPE, Date.now() - start, false)
175+
}
131176
}
132177

133178
if (Autobranch.isTriggeredBy(event) && tokens[Autobranch.TYPE]) {
134179
info(`${owner}/${name}: Executing check Autobranch`)
135-
await this.autobranch.execute(checkArgs.config, checkArgs.payload, tokens[Autobranch.TYPE])
180+
await this.checkHandler.onExecutionStart(dbRepo.id, Autobranch.TYPE, delay)
181+
try {
182+
await this.autobranch.execute(config, payload, tokens[Autobranch.TYPE])
183+
await this.checkHandler.onExecutionEnd(dbRepo.id, Autobranch.TYPE, Date.now() - start, true)
184+
} catch (e) {
185+
await this.checkHandler.onExecutionEnd(dbRepo.id, Autobranch.TYPE, Date.now() - start, false)
186+
}
136187
}
137188

138189
if (CommitMessage.isTriggeredBy(event) && tokens[CommitMessage.TYPE]) {
139190
info(`${owner}/${name}: Executing check CommitMessage`)
140-
await this.commitMessage.execute(checkArgs.config, checkArgs.payload, tokens[CommitMessage.TYPE])
191+
await this.checkHandler.onExecutionStart(dbRepo.id, CommitMessage.TYPE, delay)
192+
try {
193+
await this.commitMessage.execute(config, payload, tokens[CommitMessage.TYPE])
194+
await this.checkHandler.onExecutionEnd(dbRepo.id, CommitMessage.TYPE, Date.now() - start, true)
195+
} catch (e) {
196+
await this.checkHandler.onExecutionEnd(dbRepo.id, CommitMessage.TYPE, Date.now() - start, false)
197+
}
141198
}
142199

143200
if (PullRequestTasks.isTriggeredBy(event) && tokens[PullRequestTasks.TYPE]) {
144201
info(`${owner}/${name}: Executing check PullRequestTasks`)
145-
await this.pullRequestTasks.execute(checkArgs.config, checkArgs.payload, tokens[PullRequestTasks.TYPE])
202+
await this.checkHandler.onExecutionStart(dbRepo.id, PullRequestTasks.TYPE, delay)
203+
try {
204+
await this.pullRequestTasks.execute(config, payload, tokens[PullRequestTasks.TYPE])
205+
await this.checkHandler.onExecutionEnd(dbRepo.id, PullRequestTasks.TYPE, Date.now() - start, true)
206+
} catch (e) {
207+
await this.checkHandler.onExecutionEnd(dbRepo.id, PullRequestTasks.TYPE, Date.now() - start, false)
208+
}
146209
}
147210
}
148211
}

server/handler/CheckHandler.js

+33-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Sequelize from 'sequelize'
12
import CheckHandlerError, { CHECK_NOT_FOUND, CHECK_EXISTS, DATABASE_ERROR } from './CheckHandlerError'
23
import { Check } from '../model'
34
import { githubService } from '../service/GithubService'
@@ -21,7 +22,7 @@ function findHookEventsFor(types) {
2122
.filter((evt, i, arr) => i === arr.lastIndexOf(evt)) // deduplicate
2223
}
2324

24-
class CheckHandler {
25+
export class CheckHandler {
2526
constructor(github = githubService) {
2627
this.github = github
2728
}
@@ -139,9 +140,39 @@ class CheckHandler {
139140
const evts = findHookEventsFor(types)
140141

141142
await this.github.updateWebhookFor(repo.owner.login, repo.name, evts, user.accessToken)
142-
await checkHandler.onDeleteCheck(repo.id, type)
143+
await this.onDeleteCheck(repo.id, type)
143144
info(`${repo.full_name}: disabled check ${type}`)
144145
}
146+
147+
onExecutionStart(repoId, type, delay) {
148+
// set last_execution_ts to now
149+
// set last execution delay
150+
return Check.update({
151+
last_execution_ts: Sequelize.fn('NOW'),
152+
last_execution_delay_ms: delay,
153+
last_execution_successful: null,
154+
last_execution_ms: null
155+
}, {
156+
where: {
157+
repositoryId: repoId,
158+
type
159+
}
160+
})
161+
}
162+
163+
onExecutionEnd(repoId, type, executionTime, executionSuccess) {
164+
// set last execution ms
165+
// set last execution success = true
166+
return Check.update({
167+
last_execution_successful: executionSuccess,
168+
last_execution_ms: executionTime
169+
}, {
170+
where: {
171+
repositoryId: repoId,
172+
type
173+
}
174+
})
175+
}
145176
}
146177

147178
export const checkHandler = new CheckHandler()

server/model/Check.js

+16
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ export default db.define('check', {
3131
type: Sequelize.TEXT,
3232
allowNull: true
3333
},
34+
last_execution_delay_ms: {
35+
type: Sequelize.INTEGER,
36+
allowNull: true
37+
},
38+
last_execution_ts: {
39+
type: Sequelize.DATE,
40+
allowNull: true
41+
},
42+
last_execution_ms: {
43+
type: Sequelize.INTEGER,
44+
allowNull: true
45+
},
46+
last_execution_successful: {
47+
type: Sequelize.BOOLEAN,
48+
allowNull: true
49+
},
3450
createdAt: {
3551
type: Sequelize.DATE,
3652
allowNull: false,

test/common/util.test.js

+26
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect } from 'chai'
22
import {
33
getIn,
44
setIn,
5+
findDeepInObj,
56
encode,
67
decode,
78
setDifference,
@@ -185,6 +186,31 @@ describe('common/util', () => {
185186
})
186187
})
187188

189+
describe('findDeepInObj', () => {
190+
it('works on flat objects', () => {
191+
const obj = {
192+
a: 1,
193+
b: 2,
194+
cc: 3
195+
}
196+
const result = findDeepInObj(obj, key => /^[a-z]$/.test(key))
197+
expect(result).to.deep.equal([['a', 1], ['b', 2]])
198+
})
199+
200+
it('works on deep objects', () => {
201+
const obj = {
202+
a: {
203+
b: {
204+
cc: 3
205+
}
206+
},
207+
dd: 4
208+
}
209+
const result = findDeepInObj(obj, key => /^[a-z]{2}$/.test(key))
210+
expect(result).to.deep.equal([['cc', 3], ['dd', 4]])
211+
})
212+
})
213+
188214
describe('getIn', () => {
189215
const obj = {
190216
commit: {

0 commit comments

Comments
 (0)