Skip to content
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

Convert ReusedGUID to AddonGUID, containing fks for all Addons #14406

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 0 additions & 46 deletions src/olympia/addons/management/commands/backfill_reused_guid.py

This file was deleted.

42 changes: 42 additions & 0 deletions src/olympia/addons/migrations/0005_auto_20200527_0513.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generated by Django 2.2.12 on 2020-05-27 05:13
from django.db import migrations

from olympia.amo.utils import chunked


def add_all_addon_guids_to_addon_guid(apps, schema_editor):
AddonGUID = apps.get_model('addons', 'AddonGUID')
Addon = apps.get_model('addons', 'Addon')

batch_size = 1000
existing_ids = AddonGUID.objects.all().values_list('addon_id', flat=True)
addon_qs = Addon.unfiltered.exclude(
guid=None, id__in=existing_ids).values('id', 'guid')
for addons in chunked(addon_qs, batch_size):
addonguids = [
AddonGUID(
addon_id=addon['id'], guid=addon['guid']) for addon in addons]
AddonGUID.objects.bulk_create(addonguids, batch_size)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@diox for second opinion on this migration

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a little too much for a migration... I guess we can test that on dev/stage and see if it takes too long...

In prod, we'll likely want the site in read only to ensure that no new add-ons are created before the new code is up - Otherwise there is a window between the guids being fetched and new code being deployed where add-ons would not have a corresponding AddonGUID.



class Migration(migrations.Migration):

dependencies = [
('addons', '0004_auto_20191126_1712'),
]

operations = [
migrations.AlterModelTable(
name='ReusedGUID',
table='addons_reusedguid',
),
migrations.RenameModel(
old_name='ReusedGUID',
new_name='AddonGUID',
),
migrations.AlterModelOptions(
name='addonguid',
options={},
),
migrations.RunPython(add_all_addon_guids_to_addon_guid),
]
18 changes: 18 additions & 0 deletions src/olympia/addons/migrations/0006_auto_20200527_0843.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.12 on 2020-05-27 08:43

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('addons', '0005_auto_20200527_0513'),
]

operations = [
migrations.AlterField(
model_name='addonguid',
name='guid',
field=models.CharField(db_index=True, max_length=255),
),
]
15 changes: 10 additions & 5 deletions src/olympia/addons/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,9 +715,10 @@ def initialize_addon_from_upload(cls, data, upload, channel, user):
addon.save()
timer.log_interval('6.addon_save')

if guid:
AddonGUID.objects.create(addon=addon, guid=guid)
if old_guid_addon:
old_guid_addon.update(guid=GUID_REUSE_FORMAT.format(addon.pk))
ReusedGUID.objects.create(addon=old_guid_addon, guid=guid)
log.info(f'GUID {guid} from addon [{old_guid_addon.pk}] reused '
f'by addon [{addon.pk}].')
if user:
Expand Down Expand Up @@ -2109,11 +2110,15 @@ def track_addon_status_change(addon):
.format(addon.status))


class ReusedGUID(ModelBase):
class AddonGUID(ModelBase):
"""
Addons + guids will be added to this table when a new Add-on has reused
the guid from an earlier deleted Add-on.
Addons + guids will be added to this table whenever an addon is created.
For deleted addons it will contain an fk to the Addon instance even after
Addon.guid has been set to null (i.e. when it's been reuploaded).
"""
guid = models.CharField(max_length=255, null=False)
guid = models.CharField(max_length=255, null=False, db_index=True)
addon = models.OneToOneField(
Addon, null=False, on_delete=models.CASCADE, unique=True)

class Meta:
db_table = 'addons_reusedguid'
50 changes: 1 addition & 49 deletions src/olympia/addons/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from olympia import amo
from olympia.addons.management.commands import process_addons
from olympia.addons.models import Addon, ReusedGUID, GUID_REUSE_FORMAT
from olympia.addons.models import Addon
from olympia.abuse.models import AbuseReport
from olympia.amo.tests import (
TestCase, addon_factory, user_factory, version_factory)
Expand Down Expand Up @@ -476,54 +476,6 @@ def test_basic(self, sign_file_mock):
assert sign_file_mock.call_count == 5


@pytest.mark.django_db
def test_backfill_reused_guid():
# shouldn't show up in the query but throw them in anyway
addon_factory(name='just a deleted addon', status=amo.STATUS_DELETED)
addon_factory(name='just a public addon')
# simple case - an add-on's guid is reused once.
single_reuse_deleted = addon_factory(
name='single reuse', status=amo.STATUS_DELETED)
single_reuse_addon = addon_factory(
name='single reuse', guid='single@reuse')
single_reuse_deleted.update(
guid=GUID_REUSE_FORMAT.format(single_reuse_addon.id))
# more complex case - a guid is reused multiple times.
multi_reuse_deleted_a = addon_factory(
name='multi reuse', status=amo.STATUS_DELETED)
multi_reuse_deleted_b = addon_factory(
name='multi reuse', status=amo.STATUS_DELETED)
multi_reuse_addon = addon_factory(
name='multi reuse', guid='multi@reuse')
multi_reuse_deleted_a.update(
guid=GUID_REUSE_FORMAT.format(multi_reuse_deleted_b.id))
multi_reuse_deleted_b.update(
guid=GUID_REUSE_FORMAT.format(multi_reuse_addon.id))
# a guid reuse referencing a pk that doesn't exist (addon hard delete?)
addon_factory(
name='missing reuse', status=amo.STATUS_DELETED,
guid=GUID_REUSE_FORMAT.format(999))
# reusedguid object already there
reused_exists_deleted = addon_factory(
name='reused_exists', status=amo.STATUS_DELETED)
reused_exists_addon = addon_factory(
name='reused_exists', guid='exists@reuse')
reused_exists_deleted.update(
guid=GUID_REUSE_FORMAT.format(reused_exists_addon.id))
ReusedGUID.objects.create(addon=reused_exists_deleted, guid='exists@reuse')

assert ReusedGUID.objects.count() == 1
call_command('backfill_reused_guid')
assert ReusedGUID.objects.count() == 4
qs_values = ReusedGUID.objects.all().order_by('id').values('addon', 'guid')
assert list(qs_values) == [
{'addon': reused_exists_deleted.id, 'guid': 'exists@reuse'},
{'addon': single_reuse_deleted.id, 'guid': 'single@reuse'},
{'addon': multi_reuse_deleted_a.id, 'guid': 'multi@reuse'},
{'addon': multi_reuse_deleted_b.id, 'guid': 'multi@reuse'},
]


class TestDeleteObsoleteAddons(TestCase):
def setUp(self):
# Some add-ons that shouldn't be deleted
Expand Down
7 changes: 4 additions & 3 deletions src/olympia/addons/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from olympia.addons.models import (
Addon, AddonApprovalsCounter, AddonCategory, AddonReviewerFlags, AddonUser,
AppSupport, Category, DeniedGuid, DeniedSlug, FrozenAddon, MigratedLWT,
Preview, ReusedGUID, track_addon_status_change)
Preview, AddonGUID, track_addon_status_change)
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.tests import (
TestCase, addon_factory, user_factory, version_factory)
Expand Down Expand Up @@ -2220,6 +2220,7 @@ def test_existing_guid_same_author(self):
parsed_data = parse_addon(self.upload, user=self.user)
deleted = Addon.from_upload(self.upload, [self.selected_app],
parsed_data=parsed_data)
assert AddonGUID.objects.filter(guid='guid@xpi').count() == 1
# Claim the add-on.
AddonUser(addon=deleted, user=self.user).save()
deleted.update(status=amo.STATUS_APPROVED)
Expand All @@ -2235,8 +2236,8 @@ def test_existing_guid_same_author(self):
deleted.reload()
assert addon.guid == 'guid@xpi'
assert deleted.guid == 'guid-reused-by-pk-%s' % addon.pk
assert ReusedGUID.objects.filter(guid='guid@xpi').count() == 1
assert ReusedGUID.objects.filter(guid='guid@xpi').last().addon == (
assert AddonGUID.objects.filter(guid='guid@xpi').count() == 2
assert AddonGUID.objects.filter(guid='guid@xpi').first().addon == (
deleted)

def test_old_soft_deleted_addons_and_upload_non_extension(self):
Expand Down
4 changes: 3 additions & 1 deletion src/olympia/amo/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from olympia.accounts.utils import fxa_login_url
from olympia.addons.indexers import AddonIndexer
from olympia.addons.models import (
Addon, AddonCategory, Category,
Addon, AddonCategory, AddonGUID, Category,
update_search_index as addon_update_search_index)
from olympia.amo.urlresolvers import get_url_prefix, Prefixer, set_url_prefix
from olympia.amo.storage_utils import copy_stored_file
Expand Down Expand Up @@ -736,6 +736,8 @@ def addon_factory(

# Save 4.
addon.save()
if addon.guid:
AddonGUID.objects.create(addon=addon, guid=addon.guid)

# Potentially update is_public on authors
[user.update_is_public() for user in users]
Expand Down
19 changes: 5 additions & 14 deletions src/olympia/blocklist/mlbf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from django.conf import settings
from django.core.files.storage import default_storage as storage
from django.db.models import Q
from django.utils.functional import cached_property

from filtercascade import FilterCascade
Expand Down Expand Up @@ -34,20 +33,16 @@ def fetch_blocked_from_db(cls):
blocks_guids = [block.guid for block in blocks]

file_qs = File.objects.filter(
Q(version__addon__guid__in=blocks_guids) |
Q(version__addon__reusedguid__guid__in=blocks_guids),
version__addon__addonguid__guid__in=blocks_guids,
is_signed=True,
is_webextension=True,
).order_by('version_id').values(
'version__addon__guid',
'version__addon__reusedguid__guid',
'version__addon__addonguid__guid',
'version__version',
'version_id')
addons_versions = defaultdict(list)
for file_ in file_qs:
addon_key = (
file_['version__addon__reusedguid__guid'] or
file_['version__addon__guid'])
addon_key = file_['version__addon__addonguid__guid']
addons_versions[addon_key].append(
(file_['version__version'], file_['version_id']))

Expand All @@ -70,12 +65,8 @@ def fetch_all_versions_from_db(cls, excluding_version_ids=None):

qs = (
Version.unfiltered.exclude(id__in=excluding_version_ids or ())
.values(
'addon__guid', 'addon__reusedguid__guid', 'version'))
return list(
(version['addon__reusedguid__guid'] or version['addon__guid'],
version['version'])
for version in qs)
.values_list('addon__addonguid__guid', 'version'))
return list(qs)

@classmethod
def hash_filter_inputs(cls, input_list):
Expand Down
36 changes: 19 additions & 17 deletions src/olympia/blocklist/tests/test_mlbf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from filtercascade import FilterCascade

from olympia import amo
from olympia.addons.models import GUID_REUSE_FORMAT, ReusedGUID
from olympia.addons.models import GUID_REUSE_FORMAT
from olympia.amo.tests import (
addon_factory, TestCase, user_factory, version_factory)
from olympia.blocklist.models import Block
Expand Down Expand Up @@ -71,6 +71,18 @@ def setup_data(self):
min_version='9999')

# A blocked addon has been uploaded and deleted before
reused_2_1_addon = addon_factory(
# this a previous, superceeded, addon that should be included
status=amo.STATUS_DELETED, # they should all be deleted
version_kw={'version': '2.1'},
file_kw={'is_signed': True, 'is_webextension': True})
self.addon_deleted_before_2_1_ver = reused_2_1_addon.versions.all()[0]
reused_2_5_addon = addon_factory(
# And this is an earlier addon that should also be included
status=amo.STATUS_DELETED, # they should all be deleted
version_kw={'version': '2.5'},
file_kw={'is_signed': True, 'is_webextension': True})
self.addon_deleted_before_2_5_ver = reused_2_5_addon.versions.all()[0]
current_addon = addon_factory(
version_kw={'version': '2'},
file_kw={'is_signed': True, 'is_webextension': True})
Expand All @@ -82,22 +94,12 @@ def setup_data(self):
version_factory(
addon=current_addon, version='3.0',
file_kw={'is_signed': True, 'is_webextension': True})
reused_2_1 = ReusedGUID.objects.create(
# this a previous, superceeded, addon that should be included
guid=current_addon.guid, addon=addon_factory(
guid=GUID_REUSE_FORMAT.format(current_addon.id),
version_kw={'version': '2.1'},
file_kw={'is_signed': True, 'is_webextension': True}))
self.addon_deleted_before_2_1_ver = reused_2_1.addon.versions.all()[0]
reused_2_5 = ReusedGUID.objects.create(
# And this is an earlier addon that should also be included
guid=current_addon.guid, addon=addon_factory(
guid=GUID_REUSE_FORMAT.format(
self.addon_deleted_before_2_1_ver.addon_id),
status=amo.STATUS_DELETED, # they should all be deleted tbf
version_kw={'version': '2.5'},
file_kw={'is_signed': True, 'is_webextension': True}))
self.addon_deleted_before_2_5_ver = reused_2_5.addon.versions.all()[0]
reused_2_1_addon.update(
guid=GUID_REUSE_FORMAT.format(current_addon.id))
reused_2_5_addon.update(
guid=GUID_REUSE_FORMAT.format(reused_2_1_addon.id))
reused_2_1_addon.addonguid.update(guid=current_addon.guid)
reused_2_5_addon.addonguid.update(guid=current_addon.guid)
self.addon_deleted_before_block = Block.objects.create(
guid=current_addon.guid, min_version='2.0.1', updated_by=user)

Expand Down
11 changes: 5 additions & 6 deletions src/olympia/reviewers/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
from olympia.accounts.serializers import BaseUserSerializer
from olympia.activity.models import ActivityLog, DraftComment
from olympia.addons.models import (
Addon, AddonApprovalsCounter, AddonReviewerFlags, AddonUser, DeniedGuid,
ReusedGUID)
Addon, AddonApprovalsCounter, AddonReviewerFlags, AddonUser, DeniedGuid)
from olympia.amo.storage_utils import copy_stored_file
from olympia.amo.templatetags.jinja_helpers import (
absolutify, format_date, format_datetime)
Expand Down Expand Up @@ -5188,10 +5187,10 @@ def test_reused_guid_from_previous_deleted_addon(self):
old_two = addon_factory(status=amo.STATUS_DELETED)
old_other = addon_factory(status=amo.STATUS_DELETED)
old_noguid = addon_factory(status=amo.STATUS_DELETED)
ReusedGUID.objects.create(addon=old_one, guid='reuse@')
ReusedGUID.objects.create(addon=old_two, guid='reuse@')
ReusedGUID.objects.create(addon=old_other, guid='other@')
ReusedGUID.objects.create(addon=old_noguid, guid='')
old_one.addonguid.update(guid='reuse@')
old_two.addonguid.update(guid='reuse@')
old_other.addonguid.update(guid='other@')
old_noguid.addonguid.update(guid='')
self.addon.update(guid='reuse@')

response = self.client.get(self.url)
Expand Down