This repository was archived by the owner on Oct 30, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathconfig.coffee
222 lines (177 loc) · 6.07 KB
/
config.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# guv - Scaling governor of cloud workers
# (c) 2015 The Grid
# guv may be freely distributed under the MIT license
debug = require('debug')('guv:config')
gaussian = require 'gaussian'
url = require 'url'
yaml = require 'js-yaml'
fs = require 'fs'
path = require 'path'
schemas =
roleconfig: require '../schema/roleconfig.json'
config: require '../schema/config.json'
calculateTarget = (config) ->
# Calculate the point which the process completes
# the desired percentage of jobs within
debug 'calculate target for', config.processing, config.stddev, config.deadline
tolerance = (100-config.percentile)/100
mean = config.processing
variance = config.stddev*config.stddev
d = gaussian mean, variance
ppf = -d.ppf(tolerance)
distance = mean+ppf
# Shift the point up till hits at the specified deadline
# XXX: Is it a safe assumption that variance is same for all
target = config.deadline-distance
return target
jobsInDeadline = (config) ->
return config.target / config.process_time
# Syntactical part
parse = (str) ->
o = yaml.safeLoad str
o = {} if not o
return o
serialize = (parsed) ->
return yaml.safeDump parsed
clone = (obj) ->
return JSON.parse JSON.stringify obj
configFormat = () ->
format =
shortoptions: {}
options: {}
for name, value of schemas.roleconfig.properties
o = clone value
throw new Error "Missing type for config property #{name}" if not o.type
o.name = name
format.options[name] = o
format.shortoptions[o.shorthand] = o if o.shorthand
return format
addDefaults = (format, role, c) ->
for name, option of format.options
continue if typeof option.default == 'string'
c[name] = option.default if not c[name]?
# TODO: have a way of declaring these functions in JSON schema?
c.statuspage = process.env['STATUSPAGE_ID'] if not c.statuspage
c.broker = process.env['GUV_BROKER'] if not c.broker
c.app = process.env['GUV_APP'] if not c.app
c.broker = process.env['CLOUDAMQP_URL'] if not c.broker
c.errors = [] # avoid shared ref
if role != '*'
c.worker = role if not c.worker
c.queue = role if not c.queue
c.stddev = c.processing*0.5 if not c.stddev
c.target = calculateTarget c if not c.target
if c.target <= c.processing
e = new Error "Target #{c.target.toFixed(2)}s is lower than processing time #{c.processing.toFixed(2)}s. Attempted deadline #{c.deadline}s."
debug 'target error', e
c.errors.push e # for later reporting
c.target = c.processing+0.01 # do our best
return c
normalize = (role, vars, globals) ->
format = configFormat()
retvars = {}
# Make all globals available on each role
# Note: some things don't make sense be different per-role, but simpler this way
for k, v of globals
retvars[k] = v
for name, val of vars
# Lookup canonical long name from short
name = format.shortoptions[name].name if format.shortoptions[name]?
# Defined var
f = format.options[name]
retvars[name] = val
# Inject defaults
retvars = addDefaults format, role, retvars
return retvars
loadConfig = (parsed) ->
config = {}
# Extract globals first, as they will be merged into individual roles
globalRole = '*'
parsed[globalRole] = {} if not parsed[globalRole]
config[globalRole] = normalize globalRole, parsed[globalRole], {}
for role, vars of parsed
continue if role == globalRole
config[role] = normalize role, vars, config[globalRole]
return config
parseConfig = (str) ->
parsed = parse str
return loadConfig parsed
validateConfigObject = (parsed, options) ->
tv4 = require 'tv4'
tv4.addSchema schemas.roleconfig.id, schemas.roleconfig
tv4.addSchema schemas.config.id, schemas.config
options.allowKeys = [] if not options.allowKeys
format = configFormat()
for role, vars of parsed
for name, val of vars
# Lookup canonical long name from short
# XXX: a bit hacky way to avoid duplicate definitions in the JSON schema
if format.shortoptions[name]?
longname = format.shortoptions[name].name
vars[longname] = val
delete vars[name]
if name in options.allowKeys
delete vars[name]
checkRecursive = false
banUnknownProperties = true
result = tv4.validateMultiple parsed, schemas.config.id, checkRecursive, banUnknownProperties
errors = []
for e in result.errors
role = e.dataPath.split('/')[1]
property = e.dataPath.split('/')[2]
err = new Error "#{e.message} for #{e.dataPath}"
err.role = role
err.property = property
delete e.stack
err.schemaError = e
errors.push err
return errors
validateConfig = (str, options) ->
parsed = parse str
return validateConfigObject parsed, options
estimateRates = (cfg) ->
rates = {}
for role, c of cfg
continue if role == '*'
rate = c.concurrency * c.maximum * (1 / c.processing)
rates[role] = rate
return rates
countMinimumWorkers = (cfg) ->
byType = {}
for role, c of cfg
continue if role == '*'
byType[c.dynosize] = 0 if not byType[c.dynosize]?
byType[c.dynosize] += c.minimum
return byType
countMaximumWorkers = (cfg) ->
byType = {}
for role, c of cfg
continue if role == '*'
byType[c.dynosize] = 0 if not byType[c.dynosize]?
byType[c.dynosize] += c.maximum
return byType
main = () ->
p = process.argv[2]
f = fs.readFileSync p, 'utf-8'
parsed = parseConfig f
rates = estimateRates parsed
for role, rate of rates
perMinute = (rate*60).toFixed(2)
padded = (" " + role).slice(-40)
console.log "#{padded}: #{perMinute} jobs/minute"
minimums = countMinimumWorkers parsed
console.log 'Workers minimum'
for type, value of minimums
console.log "\t#{type}: #{value} workers"
maximums = countMaximumWorkers parsed
console.log 'Workers maximum'
for type, value of maximums
console.log "\t#{type}: #{value} workers"
main() if not module.parent
exports.validate = validateConfig
exports.validateObject = validateConfigObject
exports.parse = parseConfig
exports.parseOnly = parse
exports.load = loadConfig
exports.serialize = serialize
exports.defaults = addDefaults