-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Save config on build model #4749
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
6891db8
b1b3620
27d8251
ab5ea04
5f4fb8c
9d70812
13abeda
98d26a4
660c03a
d29b6d9
7fc1e2b
8f62f03
76929ac
7ade872
6f8becc
07efa1b
715e5ed
dab5860
5aacd63
822b793
8c60760
7ed110d
2ffdbf6
5cce5d8
9d8266f
28cd935
433b709
b88837d
4723705
043f4e6
79fbb8e
f33d54a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# -*- coding: utf-8 -*- | ||
# Generated by Django 1.9.13 on 2018-10-11 14:00 | ||
from __future__ import unicode_literals | ||
|
||
from django.db import migrations, models | ||
import jsonfield.fields | ||
import readthedocs.builds.version_slug | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('builds', '0004_add-apiversion-proxy-model'), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name='build', | ||
name='config', | ||
field=jsonfield.fields.JSONField(default={}, verbose_name='Configuration used in the build'), | ||
), | ||
migrations.AlterField( | ||
model_name='build', | ||
name='error', | ||
field=models.TextField(blank=True, default='', verbose_name='Error'), | ||
), | ||
migrations.AlterField( | ||
model_name='build', | ||
name='output', | ||
field=models.TextField(blank=True, default='', verbose_name='Output'), | ||
), | ||
migrations.AlterField( | ||
model_name='build', | ||
name='state', | ||
field=models.CharField(choices=[('triggered', 'Triggered'), ('cloning', 'Cloning'), ('installing', 'Installing'), ('building', 'Building'), ('finished', 'Finished')], default='finished', max_length=55, verbose_name='State'), | ||
), | ||
migrations.AlterField( | ||
model_name='build', | ||
name='type', | ||
field=models.CharField(choices=[('html', 'HTML'), ('pdf', 'PDF'), ('epub', 'Epub'), ('man', 'Manpage'), ('dash', 'Dash')], default='html', max_length=55, verbose_name='Type'), | ||
), | ||
migrations.AlterField( | ||
model_name='version', | ||
name='privacy_level', | ||
field=models.CharField(choices=[('public', 'Public'), ('protected', 'Protected'), ('private', 'Private')], default='public', help_text='Level of privacy for this Version.', max_length=20, verbose_name='Privacy Level'), | ||
), | ||
migrations.AlterField( | ||
model_name='version', | ||
name='slug', | ||
field=readthedocs.builds.version_slug.VersionSlugField(db_index=True, max_length=255, populate_from='verbose_name', verbose_name='Slug'), | ||
), | ||
migrations.AlterField( | ||
model_name='version', | ||
name='type', | ||
field=models.CharField(choices=[('branch', 'Branch'), ('tag', 'Tag'), ('unknown', 'Unknown')], default='unknown', max_length=20, verbose_name='Type'), | ||
), | ||
migrations.AlterField( | ||
model_name='versionalias', | ||
name='from_slug', | ||
field=models.CharField(default='', max_length=255, verbose_name='From slug'), | ||
), | ||
migrations.AlterField( | ||
model_name='versionalias', | ||
name='to_slug', | ||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='To slug'), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,36 +2,56 @@ | |
"""Models for the builds app.""" | ||
|
||
from __future__ import ( | ||
absolute_import, division, print_function, unicode_literals) | ||
absolute_import, | ||
division, | ||
print_function, | ||
unicode_literals, | ||
) | ||
|
||
import logging | ||
import os.path | ||
import re | ||
from builtins import object | ||
from shutil import rmtree | ||
|
||
from builtins import object | ||
from django.conf import settings | ||
from django.core.urlresolvers import reverse | ||
from django.db import models | ||
from django.utils.encoding import python_2_unicode_compatible | ||
from django.utils.translation import ugettext_lazy as _ | ||
from django.utils.translation import ugettext | ||
from django.utils.translation import ugettext_lazy as _ | ||
from guardian.shortcuts import assign | ||
from jsonfield import JSONField | ||
from taggit.managers import TaggableManager | ||
|
||
from readthedocs.core.utils import broadcast | ||
from readthedocs.projects.constants import ( | ||
BITBUCKET_URL, GITHUB_URL, GITLAB_URL, PRIVACY_CHOICES, PRIVATE) | ||
BITBUCKET_URL, | ||
GITHUB_URL, | ||
GITLAB_URL, | ||
PRIVACY_CHOICES, | ||
PRIVATE, | ||
) | ||
from readthedocs.projects.models import APIProject, Project | ||
|
||
from .constants import ( | ||
BRANCH, BUILD_STATE, BUILD_STATE_FINISHED, BUILD_TYPES, LATEST, | ||
NON_REPOSITORY_VERSIONS, STABLE, TAG, VERSION_TYPES) | ||
BRANCH, | ||
BUILD_STATE, | ||
BUILD_STATE_FINISHED, | ||
BUILD_TYPES, | ||
LATEST, | ||
NON_REPOSITORY_VERSIONS, | ||
STABLE, | ||
TAG, | ||
VERSION_TYPES, | ||
) | ||
from .managers import VersionManager | ||
from .querysets import BuildQuerySet, RelatedBuildQuerySet, VersionQuerySet | ||
from .utils import ( | ||
get_bitbucket_username_repo, get_github_username_repo, | ||
get_gitlab_username_repo) | ||
get_bitbucket_username_repo, | ||
get_github_username_repo, | ||
get_gitlab_username_repo, | ||
) | ||
from .version_slug import VersionSlugField | ||
|
||
DEFAULT_VERSION_PRIVACY_LEVEL = getattr( | ||
|
@@ -109,6 +129,15 @@ def __str__(self): | |
pk=self.pk, | ||
)) | ||
|
||
@property | ||
def config(self): | ||
last_build = ( | ||
self.builds.filter(type='html', state='finished') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we still use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, I was thinking the same. |
||
.order_by('-date') | ||
.first() | ||
) | ||
return last_build.config | ||
stsewd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@property | ||
def commit_name(self): | ||
""" | ||
|
@@ -448,6 +477,7 @@ class Build(models.Model): | |
exit_code = models.IntegerField(_('Exit code'), null=True, blank=True) | ||
commit = models.CharField( | ||
_('Commit'), max_length=255, null=True, blank=True) | ||
config = JSONField(_('Configuration used in the build'), default={}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this, but I think this will increase a lot the size of the db with not very useful data (because it will become old/inaccurate pretty soon). On the other hand, moving this to the cold storage I think that doesn't make sense since finally we won't be able to query these models and get the data we want. Because of this problem, I was suggesting to save this config in the Besides, answering the question: "how many project use I want to make this stored data useful, help us to debug problems but also to make decisions when we need to know how users are using our own platform: "how much used is the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using the JSON example generated from the test and checking how many builds we do have per day, I can estimate that we will have an increase of 1.5Mb per day.
(please, verify my math and logic) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1.5MB per day feels like not that much storage, to be honest. It's less about 500MB a year, give or take. Of course, doing this kind of stuff is how we keep filling up our DB, so probably best not to take the naive approach. I do see value in having recent changes of the config files stored. I wonder if there is a good way to store it on the Version, but keep a historical copy of all changes. We don't want to keep 500 copies of the same exact data structure, but being able to diff them over time feels very useful, especially when a build goes from passing to failing, or similar. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That might be overkill, perhaps we could just only save the config when it differs from the previously found one on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if there is an easy way, I'd like to have this to show to users the configuration that was used on each build, if we don't save the full config we need to query the previous one before saving (this is easy) to see if they differ. If we want to show the config for a specific build, we need to know the previous one. So, maybe another model? Or maybe a foreign key to the previous There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally prefer to take an action about this before merging. @ericholscher eric I like your proposal. I think it's "simple enough" to implement and shouldn't complicate things too much if we save "the whole config" if "we detect that differs", instead of saving a diff that later will be more complicated to generate the real config JSON text. This way, we can query "what's the latest build of this project where the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a mutable object as default it's a problem. You need to change it to Reference: https://docs.djangoproject.com/es/2.1/ref/models/fields/#django.db.models.Field.default There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I read this rpkilby/jsonfield#125, but we can change it just to be 100% sure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Change it, please. That's not the default behavior.. |
||
|
||
length = models.IntegerField(_('Build Length'), null=True, blank=True) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -233,6 +233,21 @@ def python_full_version(self): | |
) | ||
return ver | ||
|
||
def as_dict(self): | ||
attributes = [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if this logic should live somewhere else, as a |
||
'version', 'formats', 'python', | ||
'conda', 'build', 'doctype', | ||
'sphinx', 'mkdocs', 'submodules', | ||
] | ||
config = {} | ||
for name in attributes: | ||
attr = getattr(self, name) | ||
if hasattr(attr, '_asdict'): | ||
config[name] = attr._asdict() | ||
else: | ||
config[name] = attr | ||
return config | ||
|
||
def __getattr__(self, name): | ||
"""Raise an error for unknown attributes.""" | ||
raise ConfigOptionNotSupportedError(name) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -841,6 +841,60 @@ def test_config_filenames_regex(correct_config_filename): | |
assert re.match(CONFIG_FILENAME_REGEX, correct_config_filename) | ||
|
||
|
||
def test_as_dict(tmpdir): | ||
apply_fs(tmpdir, {'requirements.txt': ''}) | ||
build = get_build_config( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name of the variable is tricky. This is not a build object, but a config object. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. Feel free to do whatever you prefer. In case you think this should be renamed, open an issue in the Cleanup milestone for the "future". |
||
{ | ||
'version': 1, | ||
'formats': ['pdf'], | ||
'python': { | ||
'version': 3.5, | ||
}, | ||
'requirements_file': 'requirements.txt', | ||
}, | ||
get_env_config({ | ||
'defaults': { | ||
'doctype': 'sphinx', | ||
'sphinx_configuration': None, | ||
}, | ||
}), | ||
source_file=str(tmpdir.join('readthedocs.yml')), | ||
) | ||
build.validate() | ||
expected_dict = { | ||
'version': '1', | ||
'formats': ['pdf'], | ||
'python': { | ||
'version': 3.5, | ||
'requirements': 'requirements.txt', | ||
'install_with_pip': False, | ||
'install_with_setup': False, | ||
'extra_requirements': [], | ||
'use_system_site_packages': False, | ||
}, | ||
'build': { | ||
'image': 'readthedocs/build:2.0', | ||
}, | ||
'conda': None, | ||
'sphinx': { | ||
'builder': 'sphinx', | ||
'configuration': None, | ||
'fail_on_warning': False, | ||
}, | ||
'mkdocs': { | ||
'configuration': None, | ||
'fail_on_warning': False, | ||
}, | ||
'doctype': 'sphinx', | ||
'submodules': { | ||
'include': ALL, | ||
'exclude': [], | ||
'recursive': True, | ||
}, | ||
} | ||
assert build.as_dict() == expected_dict | ||
|
||
|
||
class TestBuildConfigV2(object): | ||
|
||
def get_build_config(self, config, env_config=None, | ||
|
@@ -1804,3 +1858,47 @@ def test_pop_config_raise_exception(self): | |
build.pop_config('one.four', raise_ex=True) | ||
assert excinfo.value.value == 'four' | ||
assert excinfo.value.code == VALUE_NOT_FOUND | ||
|
||
def test_as_dict(self, tmpdir): | ||
apply_fs(tmpdir, {'requirements.txt': ''}) | ||
build = self.get_build_config( | ||
{ | ||
'version': 2, | ||
'formats': ['pdf'], | ||
'python': { | ||
'version': 3.6, | ||
'requirements': 'requirements.txt', | ||
}, | ||
}, | ||
source_file=str(tmpdir.join('readthedocs.yml')), | ||
) | ||
build.validate() | ||
expected_dict = { | ||
'version': '2', | ||
'formats': ['pdf'], | ||
'python': { | ||
'version': 3.6, | ||
'requirements': str(tmpdir.join('requirements.txt')), | ||
'install_with_pip': False, | ||
'install_with_setup': False, | ||
'extra_requirements': [], | ||
'use_system_site_packages': False, | ||
}, | ||
'build': { | ||
'image': 'readthedocs/build:latest', | ||
}, | ||
'conda': None, | ||
'sphinx': { | ||
'builder': 'sphinx', | ||
'configuration': None, | ||
'fail_on_warning': False, | ||
}, | ||
'mkdocs': None, | ||
'doctype': 'sphinx', | ||
'submodules': { | ||
'include': [], | ||
'exclude': ALL, | ||
'recursive': False, | ||
}, | ||
} | ||
assert build.as_dict() == expected_dict |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -112,6 +112,7 @@ class BuildSerializer(serializers.ModelSerializer): | |
version_slug = serializers.ReadOnlyField(source='version.slug') | ||
docs_url = serializers.ReadOnlyField(source='version.get_absolute_url') | ||
state_display = serializers.ReadOnlyField(source='get_state_display') | ||
config = serializers.JSONField(required=False) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need this because of rpkilby/jsonfield#188 (comment) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably should be a code comment |
||
|
||
class Meta(object): | ||
model = Build | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder how long this query will take. Our build table is pretty large, but perhaps the filtering will make it a bit faster. Feels like something we might want to denormalize if we use it a lot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't use this, for now, we'll see if we can cache this maybe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is only used in the tests. Do we need this at all? Did you add it as a helper for debugging or so? Does it worth to keep it here? Where do you think it will be used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be used on the api and resolver, it'll be used on the second part of this PR