]> git.ipfire.org Git - pbs.git/commitdiff
users: Introduce daily build quotas
authorMichael Tremer <michael.tremer@ipfire.org>
Wed, 20 Sep 2023 15:23:54 +0000 (15:23 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Wed, 20 Sep 2023 15:23:54 +0000 (15:23 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
src/buildservice/jobs.py
src/buildservice/users.py
src/database.sql
src/templates/jobs/modules/list.html

index b0299d51735c570a0c0c8db99b4fb65b81d27f26..6ccb2f3e8e63362342034e3c53d8f3246694e826 100644 (file)
@@ -20,8 +20,12 @@ from .errors import *
 # Setup logging
 log = logging.getLogger("pbs.jobs")
 
-JOB_QUEUE_CTE = """
-       WITH job_queue AS (
+WITH_JOB_QUEUE_CTE = """
+       -- Determine all users which exceed their quotas
+       %s,
+
+       -- Collect all jobs and order them by priority
+       job_queue AS (
                SELECT
                        jobs.*,
                        rank() OVER (
@@ -54,10 +58,13 @@ JOB_QUEUE_CTE = """
                        jobs.finished_at IS NULL
                AND
                        jobs.installcheck_succeeded IS TRUE
+               -- Remove any jobs from users that have exceeded their quota
+               AND
+                       NOT builds.owner_id IN (SELECT user_id FROM users_with_exceeded_quotas)
                ORDER BY
                        _rank
        )
-"""
+""" % users.WITH_EXCEEDED_QUOTAS_CTE
 
 class Jobs(base.Object):
        connections = {}
@@ -223,13 +230,13 @@ class Queue(base.Object):
 
        def __len__(self):
                res = self.db.get("""
-                       %s
+                       WITH %s
 
                        SELECT
                                COUNT(*) AS len
                        FROM
                                job_queue
-                       """ % JOB_QUEUE_CTE)
+                       """ % WITH_JOB_QUEUE_CTE)
 
                if res:
                        return res.len
@@ -238,7 +245,7 @@ class Queue(base.Object):
 
        def get_jobs(self, limit=None):
                jobs = self.backend.jobs._get_jobs("""
-                       %s
+                       WITH %s
 
                        SELECT
                                *
@@ -246,7 +253,7 @@ class Queue(base.Object):
                                job_queue
                        LIMIT
                                %%s
-                       """ % JOB_QUEUE_CTE, limit,
+                       """ % WITH_JOB_QUEUE_CTE, limit,
                )
 
                return list(jobs)
@@ -256,7 +263,7 @@ class Queue(base.Object):
                        Returns all jobs that the given builder can process.
                """
                return self.backend.jobs._get_jobs("""
-                       %s
+                       WITH %s
 
                        SELECT
                                *
@@ -264,7 +271,7 @@ class Queue(base.Object):
                                job_queue
                        WHERE
                                job_queue.arch = ANY(%%s)
-                       """ % JOB_QUEUE_CTE, builder.supported_arches,
+                       """ % WITH_JOB_QUEUE_CTE, builder.supported_arches,
                )
 
        async def dispatch(self):
@@ -736,6 +743,17 @@ class Job(base.DataObject):
                """
                return self.is_pending(installcheck=True)
 
+       def is_halted(self):
+               # Only scratch builds can be halted
+               if not self.build.owner:
+                       return False
+
+               if self.is_running() or self.has_finished() or self.is_aborted():
+                       return False
+
+               # Halt if users have exceeded their quota
+               return self.build.owner.has_exceeded_build_quota()
+
        def is_running(self):
                """
                        Returns True if this job is running
index c287317f33fd4e5542467aa3318f122d42a71f8d..ebc3bb3101dcb502fb9ebf9474f15ff7cf619070 100644 (file)
@@ -52,6 +52,38 @@ LDAP_ATTRS = (
        "mailAlternateAddress",
 )
 
+WITH_EXCEEDED_QUOTAS_CTE = """
+       users_with_exceeded_quotas AS (
+               SELECT
+                       users.id AS user_id,
+                       SUM(jobs.finished_at - jobs.started_at) AS used_quota
+               FROM
+                       users
+               LEFT JOIN
+                       builds ON users.id = builds.owner_id
+               LEFT JOIN
+                       jobs ON builds.id = jobs.build_id
+               WHERE
+                       users.deleted_at IS NULL
+               AND
+                       users.daily_build_quota IS NOT NULL
+               AND
+                       builds.deleted_at IS NULL
+               AND
+                       jobs.deleted_at IS NULL
+               AND
+                       jobs.started_at IS NOT NULL
+               AND
+                       jobs.finished_at IS NOT NULL
+               AND
+                       jobs.finished_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
+               GROUP BY
+                       users.id
+               HAVING
+                       SUM(jobs.finished_at - jobs.started_at) >= users.daily_build_quota
+       )
+"""
+
 class QuotaExceededError(Exception):
        pass
 
@@ -637,6 +669,34 @@ class User(base.DataObject):
                if self.data.bugzilla_api_key:
                        return bugtracker.Bugzilla(self.backend, self.data.bugzilla_api_key)
 
+       # Build Quota
+
+       def get_daily_build_quota(self):
+               return self.data.build_quota
+
+       def set_daily_build_quota(self, quota):
+               self._set_attribute("daily_build_quota", quota)
+
+       daily_build_quota = property(get_daily_build_quota, set_daily_build_quota)
+
+       def has_exceeded_build_quota(self):
+               res = self.db.get("""
+                       WITH %s
+
+                       SELECT
+                               1 AS result
+                       FROM
+                               users_with_exceeded_quotas
+                       WHERE
+                               users_with_exceeded_quotas.user_id = %%s
+                       """ % WITH_EXCEEDED_QUOTAS_CTE, self.id,
+               )
+
+               if res and res.result:
+                       return True
+
+               return False
+
        # Storage Quota
 
        def get_storage_quota(self):
index 33f64f647b7effb06344e93f7738e67432a6771b..ba33e806909190851af74b8e2fb40077ea992765 100644 (file)
@@ -1123,7 +1123,8 @@ CREATE TABLE public.users (
     storage_quota bigint,
     perms text[] DEFAULT ARRAY[]::text[] NOT NULL,
     _attrs bytea,
-    bugzilla_api_key text
+    bugzilla_api_key text,
+    daily_build_quota interval
 );
 
 
index bb69b8082bcfefd491b706502117ce22c0e801c7..c11c0cf7b7900d0c59492cef2d8a3df3c4c266e0 100644 (file)
@@ -34,7 +34,9 @@
                                        </div>
 
                                        <div class="level-right">
-                                               {% if job.is_queued() %}
+                                               {% if job.is_halted() %}
+                                                       <span class="tag">{{ _("Halted") }}</span>
+                                               {% elif job.is_queued() %}
                                                        <span class="tag">{{ _("Queued") }}</span>
                                                {% elif job.is_pending(installcheck=False) %}
                                                        <span class="tag">{{ _("Dependency Problems") }}</span>