Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion errata/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ def parse_osv_dev_data(self, osv_dev_json):
def add_fixed_packages(self, packages):
for package in packages:
self.fixed_packages.add(package)
self.save()

def add_affected_packages(self, packages):
for package in packages:
Expand Down
19 changes: 19 additions & 0 deletions errata/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,22 @@ def test_counts_match_actual_m2m(self):
self.erratum.refresh_from_db()
self.assertEqual(self.erratum.cves_count, self.erratum.cves.count())
self.assertEqual(self.erratum.osreleases_count, self.erratum.osreleases.count())

def test_add_fixed_packages_no_stale_save(self):
"""Test that add_fixed_packages does not overwrite cached counts.

Regression test: add_fixed_packages previously called self.save()
after the M2M .add() loop, which overwrote the signal-updated
fixed_packages_count with the stale in-memory value.
"""
from arch.models import PackageArchitecture
from packages.models import Package, PackageName
pkg_arch = PackageArchitecture.objects.create(name='amd64')
pkg_name = PackageName.objects.create(name='libssl3')
pkg = Package.objects.create(
name=pkg_name, arch=pkg_arch, epoch='',
version='3.0.1', release='1', packagetype='D'
)
self.erratum.add_fixed_packages({pkg})
self.erratum.refresh_from_db()
self.assertEqual(self.erratum.fixed_packages_count, 1)
63 changes: 63 additions & 0 deletions hosts/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,66 @@ def test_get_num_security_updates(self):
self.assertEqual(self.host.get_num_security_updates(), 1)
self.assertEqual(self.host.get_num_bugfix_updates(), 1)
self.assertEqual(self.host.get_num_updates(), 2)

def test_cached_counts_survive_full_save(self):
"""Test that cached update counts are not overwritten by a full save.

Regression test: M2M signals update cached count fields via
save(update_fields=[...]), but a subsequent full save() on the
same in-memory instance would overwrite them with stale values.
"""
pkg_arch = PackageArchitecture.objects.create(name='amd64')
pkg_name = PackageName.objects.create(name='curl')
old_pkg = Package.objects.create(
name=pkg_name, arch=pkg_arch, epoch='',
version='7.0.0', release='1', packagetype='D'
)
new_pkg = Package.objects.create(
name=pkg_name, arch=pkg_arch, epoch='',
version='7.0.1', release='1', packagetype='D'
)
sec_update = PackageUpdate.objects.create(
oldpackage=old_pkg, newpackage=new_pkg, security=True
)

# simulate the find_host_updates_homogenous pattern:
# M2M add fires signal, then full save follows
self.host.updates.add(sec_update)
self.host.refresh_from_db(fields=['sec_updates_count', 'bug_updates_count'])
self.host.updated_at = timezone.now()
self.host.save()

# re-read from DB to verify counts persisted
self.host.refresh_from_db()
self.assertEqual(self.host.sec_updates_count, 1)
self.assertEqual(self.host.bug_updates_count, 0)

def test_in_memory_counts_stale_after_m2m_signal(self):
"""Test that in-memory instance has updated counts after M2M signal.

The M2M signal updates the DB directly via save(update_fields=[...]).
Django also updates the in-memory instance, but on MySQL with
REPEATABLE READ isolation and full save(), the stale snapshot can
overwrite the DB values. The refresh_from_db pattern in
find_host_updates_homogenous prevents this.
"""
pkg_arch = PackageArchitecture.objects.create(name='amd64')
pkg_name = PackageName.objects.create(name='wget')
old_pkg = Package.objects.create(
name=pkg_name, arch=pkg_arch, epoch='',
version='1.0.0', release='1', packagetype='D'
)
new_pkg = Package.objects.create(
name=pkg_name, arch=pkg_arch, epoch='',
version='1.0.1', release='1', packagetype='D'
)
bug_update = PackageUpdate.objects.create(
oldpackage=old_pkg, newpackage=new_pkg, security=False
)

# M2M add fires signal — DB has correct count
self.host.updates.add(bug_update)

# verify DB has the correct value
db_host = Host.objects.get(pk=self.host.pk)
self.assertEqual(db_host.bug_updates_count, 1)
7 changes: 6 additions & 1 deletion hosts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ def get_or_create_host(report, arch, osvariant, domain):
host.reboot_required = True
else:
host.reboot_required = False
host.save()
host.save(update_fields=[
'ipaddress', 'kernel', 'arch', 'osvariant',
'domain', 'lastreport', 'reboot_required',
])
except IntegrityError as e:
error_message(text=e)
if host:
Expand All @@ -94,6 +97,7 @@ def find_host_updates_homogenous(hosts, verbose=False):
host.find_updates()
if verbose:
info_message(text='')
host.refresh_from_db(fields=['sec_updates_count', 'bug_updates_count', 'errata_count'])
host.updated_at = ts
host.save()

Expand All @@ -114,6 +118,7 @@ def find_host_updates_homogenous(hosts, verbose=False):
continue

fhost.updates.set(updates)
fhost.refresh_from_db(fields=['sec_updates_count', 'bug_updates_count'])
fhost.updated_at = ts
fhost.save()
updated_host_ids.add(fhost.id)
Expand Down