# XXX implement starting builders if needed
+ await self._autoscale_start()
+
+ async def _autoscale_start(self, wait=False):
+ # XXX max queue length
+ threshold = datetime.timedelta(minutes=5)
+
+ # Fetch all enabled builders and whether they are running or not
+ builders = {
+ builder : await builder.is_running() for builder in self if builder.enabled
+ }
+
+ # Sort all running builders to the beginning of the list
+ builders = sorted(builders, key=lambda b: (builders[b], b))
+
+ # Store the length of the queue for each builder
+ queue = {
+ builder : datetime.timedelta(0) for builder in builders
+ }
+
+ # Run through all build jobs and allocate them to a builder.
+ # If a builder is full (i.e. reaches the threshold of its build time),
+ # we move on to the next builder until that is full and so on.
+ for job in self.backend.jobqueue:
+ log.debug("Processing job %s..." % job)
+
+ for builder in builders:
+ # Skip disabled builders
+ if not builder.enabled:
+ continue
+
+ # Check if this builder is already at capacity
+ if queue[builder] / builder.max_jobs >= threshold:
+ log.debug("Builder %s is already full" % builder)
+ continue
+
+ # Skip if this builder cannot build this job
+ if not builder.can_build(job):
+ continue
+
+ log.debug("Builder %s can build %s" % (builder, job))
+
+ # Add the job to the total build time
+ queue[builder] += job.estimated_build_time
+ break
+
+ # Start all builders that have been allocated at least one job
+ await asyncio.gather(
+ *(builder.start(wait=wait) for builder in builders if queue[builder]),
+ )
+
class Builder(base.DataObject):
table = "builders"
return NotImplemented
+ def __repr__(self):
+ return "<%s %s>" % (self.__class__.__name__, self.hostname)
+
def __str__(self):
return self.hostname
return sorted(arches)
+ def can_build(self, job):
+ return job.arch in self.supported_arches
+
def set_testmode(self, testmode):
self._set_attribute("testmode", testmode)
if self.instance:
return self.instance.state.get("Name")
- async def start(self):
+ async def start(self, wait=True):
"""
Starts the instance on AWS
"""
- await asyncio.to_thread(self._start)
+ await asyncio.to_thread(self._start, wait=wait)
- def _start(self):
+ def _start(self, wait):
log.info("Starting %s" % self)
# Set correct instance type
# Send the start signal
self.instance.start()
+ # End here if we don't want to wait
+ if not wait:
+ return
+
log.debug("Waiting until %s has started" % self)
# And wait until the instance is running
ALTER TABLE public.packages OWNER TO pakfire;
+--
+-- Name: package_estimated_build_times; Type: VIEW; Schema: public; Owner: pakfire
+--
+
+CREATE VIEW public.package_estimated_build_times AS
+ SELECT packages.name,
+ jobs.arch,
+ avg((jobs.time_finished - jobs.time_started)) AS build_time
+ FROM ((public.jobs
+ LEFT JOIN public.builds ON ((jobs.build_id = builds.id)))
+ LEFT JOIN public.packages ON ((builds.pkg_id = packages.id)))
+ WHERE ((jobs.state = 'finished'::text) AND (jobs.test IS FALSE) AND (jobs.time_started IS NOT NULL) AND (jobs.time_finished IS NOT NULL))
+ GROUP BY packages.name, jobs.arch;
+
+
+ALTER TABLE public.package_estimated_build_times OWNER TO pakfire;
+
+--
+-- Name: VIEW package_estimated_build_times; Type: COMMENT; Schema: public; Owner: pakfire
+--
+
+COMMENT ON VIEW public.package_estimated_build_times IS 'Should add this later: AND jobs.time_finished >= (CURRENT_TIMESTAMP - ''180 days''::interval)';
+
+
--
-- Name: packages_deps; Type: TABLE; Schema: public; Owner: pakfire
--