launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24737
[Merge] ~twom/launchpad:garboci into launchpad:master
Tom Wardill has proposed merging ~twom/launchpad:garboci into launchpad:master.
Commit message:
Add garbage collection for OCIFile
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~twom/launchpad/+git/launchpad/+merge/384121
Adding a column for date_last_used, delete any OCIFiles and the matching librarian files that haven't been used in an image for the last 7 days.
I think this should be safe, as the buildbehaviour updates the date_last_used on retrieval from the buildd. As long as there isn't more than 7 days between retrieval and upload, the files should still be available to put on disk before upload.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~twom/launchpad:garboci into launchpad:master.
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index f89f620..93dc9f8 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -997,7 +997,7 @@ public.libraryfilecontent = SELECT, INSERT
public.livefs = SELECT
public.livefsbuild = SELECT, UPDATE
public.livefsfile = SELECT
-public.ocifile = SELECT
+public.ocifile = SELECT, UPDATE
public.ociproject = SELECT
public.ociprojectname = SELECT
public.ocipushrule = SELECT
@@ -2440,6 +2440,7 @@ public.livefsfile = SELECT, DELETE
public.logintoken = SELECT, DELETE
public.mailinglistsubscription = SELECT, DELETE
public.milestonetag = SELECT
+public.ocifile = SELECT, DELETE
public.ociproject = SELECT
public.ociprojectseries = SELECT
public.ocirecipe = SELECT
diff --git a/lib/lp/oci/interfaces/ocirecipebuild.py b/lib/lp/oci/interfaces/ocirecipebuild.py
index 95539c0..5d2b3d5 100644
--- a/lib/lp/oci/interfaces/ocirecipebuild.py
+++ b/lib/lp/oci/interfaces/ocirecipebuild.py
@@ -83,13 +83,6 @@ class OCIRecipeBuildRegistryUploadStatus(EnumeratedType):
""")
-class IOCIFileSet(Interface):
- """A file artifact of an OCIRecipeBuild."""
-
- def getByLayerDigest(layer_file_digest):
- """Return an `IOCIFile` with the matching layer_file_digest."""
-
-
class IOCIRecipeBuildView(IPackageBuild):
"""`IOCIRecipeBuild` attributes that require launchpad.View permission."""
@@ -256,3 +249,15 @@ class IOCIFile(Interface):
"a build to a registry. This hash is in an opaque format "
"generated by the OCI build tool."),
required=False, readonly=True)
+
+ date_last_used = Datetime(
+ title=_("The datetime this file was last used in a build."),
+ required=True,
+ readonly=False)
+
+
+class IOCIFileSet(Interface):
+ """A file artifact of an OCIRecipeBuild."""
+
+ def getByLayerDigest(layer_file_digest):
+ """Return an `IOCIFile` with the matching layer_file_digest."""
diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py
index 94b7326..de6ed3c 100644
--- a/lib/lp/oci/model/ocirecipebuild.py
+++ b/lib/lp/oci/model/ocirecipebuild.py
@@ -91,6 +91,9 @@ class OCIFile(Storm):
layer_file_digest = Unicode(name='layer_file_digest', allow_none=True)
+ date_last_used = DateTime(
+ name='date_last_used', tzinfo=pytz.UTC, allow_none=False)
+
def __init__(self, build, library_file, layer_file_digest=None):
"""Construct a `OCIFile`."""
super(OCIFile, self).__init__()
diff --git a/lib/lp/oci/model/ocirecipebuildbehaviour.py b/lib/lp/oci/model/ocirecipebuildbehaviour.py
index 6205cfd..8b5f5e0 100644
--- a/lib/lp/oci/model/ocirecipebuildbehaviour.py
+++ b/lib/lp/oci/model/ocirecipebuildbehaviour.py
@@ -14,14 +14,16 @@ __all__ = [
]
+from datetime import datetime
import json
import os
+import pytz
from twisted.internet import defer
from zope.component import getUtility
from zope.interface import implementer
+from zope.security.proxy import removeSecurityProxy
-from lp.app.errors import NotFoundError
from lp.buildmaster.enums import BuildBaseImageType
from lp.buildmaster.interfaces.builder import (
BuildDaemonError,
@@ -141,6 +143,8 @@ class OCIRecipeBuildBehaviour(SnapProxyMixin, BuildFarmJobBehaviourBase):
digest)
if oci_file:
librarian_file = oci_file.library_file
+ unsecure_file = removeSecurityProxy(oci_file)
+ unsecure_file.date_last_used = datetime.now(pytz.UTC)
# If it doesn't, we need to download it
else:
files.add(layer_filename)
diff --git a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
index cf0056b..87917f4 100644
--- a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
+++ b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
@@ -18,6 +18,7 @@ import uuid
import fixtures
from fixtures import MockPatch
+import pytz
from six.moves.urllib_parse import urlsplit
from testtools import ExpectedException
from testtools.matchers import (
@@ -523,14 +524,17 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
@defer.inlineCallbacks
def test_handleStatus_OK_reuse_from_other_build(self):
"""We should be able to reuse a layer file from a separate build."""
- self.factory.makeOCIFile(
+ oci_file = self.factory.makeOCIFile(
layer_file_digest=u'digest_2',
content="layer 2 retrieved from librarian")
- now = datetime.now()
+ now = datetime.now(pytz.UTC)
mock_datetime = self.useFixture(MockPatch(
'lp.buildmaster.model.buildfarmjobbehaviour.datetime')).mock
+ mock_oci_datetime = self.useFixture(MockPatch(
+ 'lp.oci.model.ocirecipebuildbehaviour.datetime')).mock
mock_datetime.now = lambda: now
+ mock_oci_datetime.now = lambda tzinfo=None: now
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
self.build.buildqueue_record, 'OK',
@@ -554,6 +558,7 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
with open(layer_2_path, 'rb') as layer_2_fp:
contents = layer_2_fp.read()
self.assertEqual(contents, b'layer 2 retrieved from librarian')
+ self.assertEqual(now, oci_file.date_last_used)
@defer.inlineCallbacks
def test_handleStatus_OK_absolute_filepath(self):
diff --git a/lib/lp/scripts/garbo.py b/lib/lp/scripts/garbo.py
index 98c4b04..cf0bc4f 100644
--- a/lib/lp/scripts/garbo.py
+++ b/lib/lp/scripts/garbo.py
@@ -70,6 +70,7 @@ from lp.code.model.revision import (
RevisionCache,
)
from lp.hardwaredb.model.hwdb import HWSubmission
+from lp.oci.model.ocirecipebuild import OCIFile
from lp.registry.model.person import Person
from lp.registry.model.product import Product
from lp.registry.model.sourcepackagename import SourcePackageName
@@ -1569,6 +1570,19 @@ class SnapFilePruner(BulkPruner):
""" % (SnapBuildJobType.STORE_UPLOAD.value, JobStatus.COMPLETED.value)
+class OCIFilePruner(BulkPruner):
+ """Prune old `OCIFile`s that have expired."""
+ target_table_class = OCIFile
+ ids_to_prune_query = """
+ SELECT DISTINCT OCIFile.id
+ FROM OCIFile
+ WHERE
+ OCIFile.date_last_used <
+ CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
+ - CAST('7 days' AS INTERVAL)
+ """
+
+
class BaseDatabaseGarbageCollector(LaunchpadCronScript):
"""Abstract base class to run a collection of TunableLoops."""
script_name = None # Script name for locking and database user. Override.
@@ -1851,6 +1865,7 @@ class DailyDatabaseGarbageCollector(BaseDatabaseGarbageCollector):
HWSubmissionEmailLinker,
LiveFSFilePruner,
LoginTokenPruner,
+ OCIFilePruner,
ObsoleteBugAttachmentPruner,
OldTimeLimitedTokenDeleter,
POTranslationPruner,
diff --git a/lib/lp/scripts/tests/test_garbo.py b/lib/lp/scripts/tests/test_garbo.py
index dfeeba8..97978cd 100644
--- a/lib/lp/scripts/tests/test_garbo.py
+++ b/lib/lp/scripts/tests/test_garbo.py
@@ -69,6 +69,8 @@ from lp.code.model.gitjob import (
GitJob,
GitRefScanJob,
)
+from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE
+from lp.oci.model.ocirecipebuild import OCIFile
from lp.registry.enums import (
BranchSharingPolicy,
BugSharingPolicy,
@@ -1057,6 +1059,48 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
switch_dbuser('testadmin')
self.assertEqual(1, store.find(SnapBuildJob).count())
+ def test_OCIFileJobPruner(self):
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ # Garbo removes files that haven't been used in 7 days
+ switch_dbuser('testadmin')
+ store = IMasterStore(OCIFile)
+ ocifile = self.factory.makeOCIFile()
+ ocifile.date_last_used = THIRTY_DAYS_AGO
+ self.assertEqual(1, store.find(OCIFile).count())
+
+ self.runDaily()
+
+ switch_dbuser('testadmin')
+ self.assertEqual(0, store.find(OCIFile).count())
+
+ def test_OCIFileJobPruner_doesnt_prune_recent(self):
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ # Garbo removes files that haven't been used in 7 days
+ switch_dbuser('testadmin')
+ store = IMasterStore(OCIFile)
+ self.factory.makeOCIFile()
+ self.assertEqual(1, store.find(OCIFile).count())
+
+ self.runDaily()
+
+ switch_dbuser('testadmin')
+ self.assertEqual(1, store.find(OCIFile).count())
+
+ def test_OCIFileJobPruner_mixed_dates(self):
+ self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ # Garbo removes files that haven't been used in 7 days
+ switch_dbuser('testadmin')
+ store = IMasterStore(OCIFile)
+ ocifile = self.factory.makeOCIFile()
+ ocifile.date_last_used = THIRTY_DAYS_AGO
+ self.factory.makeOCIFile()
+ self.assertEqual(2, store.find(OCIFile).count())
+
+ self.runDaily()
+
+ switch_dbuser('testadmin')
+ self.assertEqual(1, store.find(OCIFile).count())
+
def test_WebhookJobPruner(self):
# Garbo should remove jobs completed over 30 days ago.
switch_dbuser('testadmin')