From 872539bdbff0bd7f6f9b065bc031e510581333e9 Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Thu, 12 Feb 2015 14:11:56 -0500 Subject: [PATCH] Switch to a per-namespace configurable expiration policy for time machine, and switch the tag gc to respect it. --- config.py | 3 --- data/database.py | 12 +++++++++--- data/model/legacy.py | 28 +++++++++++++++++----------- test/data/test.db | Bin 708608 -> 712704 bytes 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/config.py b/config.py index 47f836587..39a257b15 100644 --- a/config.py +++ b/config.py @@ -185,8 +185,5 @@ class DefaultConfig(object): LOG_ARCHIVE_LOCATION = 'local_us' LOG_ARCHIVE_PATH = 'logarchive/' - # Number of revisions to keep expired tags - TIME_MACHINE_DELTA_SECONDS = 14 * 24 * 60 * 60 - # For enterprise: MAXIMUM_REPOSITORY_USAGE = 20 diff --git a/data/database.py b/data/database.py index 7d027aa71..ccc5900a8 100644 --- a/data/database.py +++ b/data/database.py @@ -1,12 +1,14 @@ import string import logging import uuid +import time from random import SystemRandom from datetime import datetime from peewee import * -from data.read_slave import ReadSlaveModel from sqlalchemy.engine.url import make_url + +from data.read_slave import ReadSlaveModel from util.names import urn_generator @@ -136,6 +138,9 @@ def uuid_generator(): return str(uuid.uuid4()) +_get_epoch_timestamp = lambda: int(time.time()) + + def close_db_filter(_): if not db.is_closed(): logger.debug('Disconnecting from database.') @@ -175,6 +180,7 @@ class User(BaseModel): invoice_email = BooleanField(default=False) invalid_login_attempts = IntegerField(default=0) last_invalid_login = DateTimeField(default=datetime.utcnow) + removed_tag_expiration_s = IntegerField(default=1209600) # Two weeks def delete_instance(self, recursive=False, delete_nullable=False): # If we are deleting a robot account, only execute the subset of queries necessary. @@ -456,8 +462,8 @@ class RepositoryTag(BaseModel): name = CharField() image = ForeignKeyField(Image) repository = ForeignKeyField(Repository) - lifetime_start = DateTimeField(default=datetime.utcnow) - lifetime_end = DateTimeField(null=True) + lifetime_start_ts = IntegerField(default=_get_epoch_timestamp) + lifetime_end_ts = IntegerField(null=True, index=True) class Meta: database = db diff --git a/data/model/legacy.py b/data/model/legacy.py index 7495f4fcf..b48318b28 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -2,6 +2,7 @@ import bcrypt import logging import dateutil.parser import json +import time from datetime import datetime, timedelta, date @@ -1531,8 +1532,8 @@ def get_repository_images(namespace_name, repository_name): def _tag_alive(query): - return query.where((RepositoryTag.lifetime_end >> None) | - (RepositoryTag.lifetime_end > datetime.utcnow())) + return query.where((RepositoryTag.lifetime_end_ts >> None) | + (RepositoryTag.lifetime_end_ts > int(time.time()))) def list_repository_tags(namespace_name, repository_name): @@ -1547,14 +1548,18 @@ def list_repository_tags(namespace_name, repository_name): def _garbage_collect_tags(namespace_name, repository_name): - with config.app_config['DB_TRANSACTION_FACTORY'](db): - repo = _get_repository(namespace_name, repository_name) - collect_time = (datetime.utcnow() - - timedelta(seconds=config.app_config['TIME_MACHINE_DELTA_SECONDS'])) + to_delete = (RepositoryTag + .select(RepositoryTag.id) + .join(Repository) + .join(Namespace, on=(Repository.namespace_user == Namespace.id)) + .where(Repository.name == repository_name, Namespace.username == namespace_name, + ~(RepositoryTag.lifetime_end_ts >> None), + (RepositoryTag.lifetime_end_ts + Namespace.removed_tag_expiration_s) <= + int(time.time()))) (RepositoryTag .delete() - .where(RepositoryTag.repository == repo, RepositoryTag.lifetime_end < collect_time) + .where(RepositoryTag.id << to_delete) .execute()) @@ -1745,7 +1750,7 @@ def create_or_update_tag(namespace_name, repository_name, tag_name, except Image.DoesNotExist: raise DataModelException('Invalid image with id: %s' % tag_docker_image_id) - now = datetime.utcnow() + now_ts = int(time.time()) try: # When we move a tag, we really end the timeline of the old one and create a new one @@ -1753,13 +1758,14 @@ def create_or_update_tag(namespace_name, repository_name, tag_name, .select() .where(RepositoryTag.repository == repo, RepositoryTag.name == tag_name)) tag = query.get() - tag.lifetime_end = now + tag.lifetime_end_ts = now_ts tag.save() except RepositoryTag.DoesNotExist: # No tag that needs to be ended pass - tag = RepositoryTag.create(repository=repo, image=image, name=tag_name, lifetime_start=now) + tag = RepositoryTag.create(repository=repo, image=image, name=tag_name, + lifetime_start_ts=now_ts) return tag @@ -1781,7 +1787,7 @@ def delete_tag(namespace_name, repository_name, tag_name): (tag_name, namespace_name, repository_name)) raise DataModelException(msg) - found.lifetime_end = datetime.utcnow() + found.lifetime_end_ts = int(time.time()) found.save() diff --git a/test/data/test.db b/test/data/test.db index 1e810ea24930671a0bac6571e5c2fe3ddc4ba91d..79f6011ad0abaa36d0c7cc3756755dc482e9ad07 100644 GIT binary patch delta 9559 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 S8yz?Xv&*FjsYPFf%>NCsEbj0C delta 9997 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 E1vCr-{r~^~