A/B testing rootSets should provide commit times as well as revisions
[WebKit-https.git] / Websites / perf.webkit.org / tools / sync-with-buildbot.py
1 #!/usr/bin/python
2
3 import argparse
4 import base64
5 import copy
6 import json
7 import sys
8 import time
9 import urllib
10 import urllib2
11
12
13 def main():
14     parser = argparse.ArgumentParser()
15     parser.add_argument('--build-requests-url', required=True, help='URL for the build requests JSON API; e.g. https://perf.webkit.org/api/build-requests/build.webkit.org/')
16     parser.add_argument('--build-requests-user', help='The username for Basic Authentication to access the build requests JSON API')
17     parser.add_argument('--build-requests-password', help='The password for Basic Authentication to access the build requests JSON API')
18     parser.add_argument('--slave-name', required=True, help='The slave name used to update the build requets status')
19     parser.add_argument('--slave-password', required=True, help='The slave password used to update the build requets status')
20     parser.add_argument('--buildbot-url', required=True, help='URL for a buildbot builder; e.g. "https://build.webkit.org/"')
21     parser.add_argument('--builder-config-json', required=True, help='The path to a JSON file that specifies which test and platform will be posted to which builder. '
22         'The JSON should contain an array of dictionaries with keys "platform", "test", and "builder" '
23         'with the platform name (e.g. mountainlion), the test path (e.g. ["Parser", "html5-full-render"]), and the builder name (e.g. Apple MountainLion Release (Perf)) as values.')
24     parser.add_argument('--lookback-count', type=int, default=10, help='The number of builds to look back when finding in-progress builds on the buildbot')
25     parser.add_argument('--seconds-to-sleep', type=float, default=120, help='The seconds to sleep between iterations')
26     args = parser.parse_args()
27
28     configurations = load_config(args.builder_config_json, args.buildbot_url.strip('/'))
29     build_request_auth = {'user': args.build_requests_user, 'password': args.build_requests_password or ''} if args.build_requests_user else None
30     request_updates = {}
31     while True:
32         request_updates.update(find_request_updates(configurations, args.lookback_count))
33         if request_updates:
34             print 'Updating the build requests %s...' % ', '.join(map(str, request_updates.keys()))
35         else:
36             print 'No updates...'
37
38         payload = {'buildRequestUpdates': request_updates, 'slaveName': args.slave_name, 'slavePassword': args.slave_password}
39         response = update_and_fetch_build_requests(args.build_requests_url, build_request_auth, payload)
40         root_sets = response.get('rootSets', {})
41         open_requests = response.get('buildRequests', [])
42
43         for request in filter(lambda request: request['status'] == 'pending', open_requests):
44             config = config_for_request(configurations, request)
45             if len(config['scheduledRequests']) < 1:
46                 print "Scheduling the build request %s..." % str(request['id'])
47                 schedule_request(config, request, root_sets)
48
49         request_updates = find_stale_request_updates(configurations, open_requests, request_updates.keys())
50         if request_updates:
51             print "Found stale build requests %s..." % ', '.join(map(str, request_updates.keys()))
52
53         time.sleep(args.seconds_to_sleep)
54
55
56 def load_config(config_json_path, buildbot_url):
57     with open(config_json_path) as config_json:
58         configurations = json.load(config_json)
59
60     for config in configurations:
61         escaped_builder_name = urllib.quote(config['builder'])
62         config['url'] = '%s/builders/%s/' % (buildbot_url, escaped_builder_name)
63         config['jsonURL'] = '%s/json/builders/%s/' % (buildbot_url, escaped_builder_name)
64         config['scheduledRequests'] = set()
65
66     return configurations
67
68
69 def find_request_updates(configurations, lookback_count):
70     request_updates = {}
71
72     for config in configurations:
73         try:
74             pending_builds = fetch_json(config['jsonURL'] + 'pendingBuilds')
75             scheduled_requests = filter(None, [request_id_from_build(config, build) for build in pending_builds])
76             for request_id in scheduled_requests:
77                 request_updates[request_id] = {'status': 'scheduled', 'url': config['url']}
78             config['scheduledRequests'] = set(scheduled_requests)
79         except (IOError, ValueError) as error:
80             print >> sys.stderr, "Failed to fetch pending builds for %s: %s" % (config['builder'], str(error))
81
82     for config in configurations:
83         for i in range(1, lookback_count + 1):
84             build_error = None
85             build_index = -i
86             try:
87                 build = fetch_json(config['jsonURL'] + 'builds/%d' % build_index)
88                 request_id = request_id_from_build(config, build)
89                 if not request_id:
90                     continue
91
92                 in_progress = build.get('currentStep')
93                 if in_progress:
94                     request_updates[request_id] = {'status': 'running', 'url': config['url']}
95                     config['scheduledRequests'].discard(request_id)
96                 else:
97                     url = config['url'] + 'builds/' + str(build['number'])
98                     request_updates[request_id] = {'status': 'failedIfNotCompleted', 'url': url}
99             except urllib2.HTTPError as error:
100                 if error.code == 404:
101                     break
102                 else:
103                     build_error = error
104             except ValueError as error:
105                 build_error = error
106             if build_error:
107                 print >> sys.stderr, "Failed to fetch build %d for %s: %s" % (build_index, config['builder'], str(build_error))
108
109     return request_updates
110
111
112 def update_and_fetch_build_requests(build_requests_url, build_request_auth, payload):
113     try:
114         response = fetch_json(build_requests_url, payload=json.dumps(payload), auth=build_request_auth)
115         if response['status'] != 'OK':
116             raise ValueError(response['status'])
117         return response
118     except (IOError, ValueError) as error:
119         print >> sys.stderr, 'Failed to update or fetch build requests at %s: %s' % (build_requests_url, str(error))
120     return {}
121
122
123 def find_stale_request_updates(configurations, open_requests, requests_on_buildbot):
124     request_updates = {}
125     for request in open_requests:
126         request_id = int(request['id'])
127         should_be_on_buildbot = request['status'] in ('scheduled', 'running')
128         if should_be_on_buildbot and request_id not in requests_on_buildbot:
129             config = config_for_request(configurations, request)
130             if config:
131                 request_updates[request_id] = {'status': 'failed', 'url': config['url']}
132     return request_updates
133
134
135 def schedule_request(config, request, root_sets):
136     roots = root_sets.get(request['rootSet'], {})
137
138     payload = {}
139     for property_name, property_value in config['arguments'].iteritems():
140         if not isinstance(property_value, dict):
141             payload[property_name] = property_value
142         elif 'root' in property_value:
143             payload[property_name] = roots[property_value['root']]['revision']
144         elif 'rootsExcluding' in property_value:
145             excluded_roots = property_value['rootsExcluding']
146             filtered_roots = {}
147             for root_name in roots:
148                 if root_name not in excluded_roots:
149                     filtered_roots[root_name] = roots[root_name]
150             payload[property_name] = json.dumps(filtered_roots)
151         else:
152             print >> sys.stderr, "Failed to process an argument %s: %s" % (property_name, property_value)
153     payload[config['buildRequestArgument']] = request['id']
154
155     try:
156         urllib2.urlopen(urllib2.Request(config['url'] + 'force'), urllib.urlencode(payload))
157         config['scheduledRequests'].add(request['id'])
158     except (IOError, ValueError) as error:
159         print >> sys.stderr, "Failed to fetch pending builds for %s: %s" % (config['builder'], str(error))
160
161
162 def config_for_request(configurations, request):
163     for config in configurations:
164         if config['platform'] == request['platform'] and config['test'] == request['test']:
165             return config
166     return None
167
168
169 def fetch_json(url, auth={}, payload=None):
170     request = urllib2.Request(url)
171     if auth:
172         request.add_header('Authorization', "Basic %s" % base64.encodestring('%s:%s' % (auth['user'], auth['password'])).rstrip('\n'))
173     response = urllib2.urlopen(request, payload).read()
174     try:
175         return json.loads(response)
176     except ValueError as error:
177         raise ValueError(str(error) + '\n' + response)
178
179
180 def property_value_from_build(build, name):
181     for prop in build.get('properties', []):
182         if prop[0] == name:
183             return prop[1]
184     return None
185
186
187 def request_id_from_build(config, build):
188     job_id = property_value_from_build(build, config['buildRequestArgument'])
189     return int(job_id) if job_id and job_id.isdigit() else None
190
191
192 if __name__ == "__main__":
193     main()