Skip to content

Commit 847964a

Browse files
committed
bulk db optimizations for errata processing
- optimize scan_for_security_updates with queryset update and __in filter - add _mark_updates_security helper with bulk update and IntegrityError fallback - optimize parse_osv_dev_data with Q batch filter for affected versions - bulk M2M add for add_fixed_packages/add_affected_packages - add get_matching_packages_q for batch version lookups - batch cve adds in parse_osv_dev_data
1 parent 159ceca commit 847964a

2 files changed

Lines changed: 61 additions & 44 deletions

File tree

errata/models.py

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from errata.managers import ErratumManager
2323
from packages.models import Package, PackageUpdate
24-
from packages.utils import find_evr, get_matching_packages
24+
from packages.utils import find_evr, get_matching_packages_q
2525
from security.models import CVE, Reference
2626
from security.utils import get_or_create_cve, get_or_create_reference
2727
from util import get_url
@@ -64,34 +64,32 @@ def get_absolute_url(self):
6464

6565
def scan_for_security_updates(self):
6666
if self.e_type == 'security':
67-
for package in self.fixed_packages.all():
68-
affected_updates = PackageUpdate.objects.filter(
69-
newpackage=package,
70-
security=False,
67+
fixed_pks = list(self.fixed_packages.values_list('pk', flat=True))
68+
if fixed_pks:
69+
self._mark_updates_security(
70+
PackageUpdate.objects.filter(newpackage__in=fixed_pks, security=False)
7171
)
72-
for affected_update in affected_updates:
73-
affected_update.security = True
74-
try:
75-
affected_update.save()
76-
except IntegrityError as e:
77-
error_message(text=e)
78-
# a version of this update already exists that is
79-
# marked as a security update, so delete this one
80-
affected_update.delete()
81-
for package in self.affected_packages.all():
82-
affected_updates = PackageUpdate.objects.filter(
83-
oldpackage=package,
84-
security=False,
72+
affected_pks = list(self.affected_packages.values_list('pk', flat=True))
73+
if affected_pks:
74+
self._mark_updates_security(
75+
PackageUpdate.objects.filter(oldpackage__in=affected_pks, security=False)
8576
)
86-
for affected_update in affected_updates:
87-
affected_update.security = True
88-
try:
89-
affected_update.save()
90-
except IntegrityError as e:
91-
error_message(text=e)
92-
# a version of this update already exists that is
93-
# marked as a security update, so delete this one
94-
affected_update.delete()
77+
78+
def _mark_updates_security(self, updates):
79+
""" Mark a queryset of PackageUpdates as security updates.
80+
Handles IntegrityError by deleting duplicates.
81+
"""
82+
try:
83+
updates.update(security=True)
84+
except IntegrityError:
85+
# fall back to individual saves to handle duplicates
86+
for update in updates:
87+
update.security = True
88+
try:
89+
update.save()
90+
except IntegrityError as e:
91+
error_message(text=e)
92+
update.delete()
9593

9694
def fetch_osv_dev_data(self):
9795
osv_dev_url = f'https://api.osv.dev/v1/vulns/{self.name}'
@@ -104,15 +102,19 @@ def fetch_osv_dev_data(self):
104102
self.parse_osv_dev_data(osv_dev_json)
105103

106104
def parse_osv_dev_data(self, osv_dev_json):
105+
from django.db.models import Q
107106
name = osv_dev_json.get('id')
108107
if name != self.name:
109108
error_message(text=f'Erratum name mismatch - {self.name} != {name}')
110109
return
111110
related = osv_dev_json.get('related')
112111
if related:
112+
cves = []
113113
for vuln in related:
114114
if vuln.startswith('CVE'):
115-
self.add_cve(vuln)
115+
cves.append(vuln)
116+
for cve_id in cves:
117+
self.add_cve(cve_id)
116118
affected = osv_dev_json.get('affected')
117119
if not affected:
118120
return
@@ -129,30 +131,29 @@ def parse_osv_dev_data(self, osv_dev_json):
129131
for match in matching_packages:
130132
fixed_packages.add(match)
131133
affected_versions = package.get('versions')
132-
if not affected_versions:
134+
if not affected_versions or not fixed_packages:
133135
continue
134-
for package in fixed_packages:
136+
for fp in fixed_packages:
137+
q = Q()
135138
for version in affected_versions:
136139
epoch, ver, rel = find_evr(version)
137-
matching_packages = get_matching_packages(
138-
name=package.name,
139-
epoch=epoch,
140-
version=ver,
141-
release=rel,
142-
arch=package.arch,
143-
p_type=package.packagetype,
144-
)
145-
for match in matching_packages:
146-
affected_packages.add(match)
140+
q |= Q(epoch=epoch, version=ver, release=rel)
141+
matching_packages = get_matching_packages_q(
142+
name=fp.name,
143+
q=q,
144+
arch=fp.arch,
145+
p_type=fp.packagetype,
146+
)
147+
affected_packages.update(matching_packages)
147148
self.add_affected_packages(affected_packages)
148149

149150
def add_fixed_packages(self, packages):
150-
for package in packages:
151-
self.fixed_packages.add(package)
151+
if packages:
152+
self.fixed_packages.add(*packages)
152153

153154
def add_affected_packages(self, packages):
154-
for package in packages:
155-
self.affected_packages.add(package)
155+
if packages:
156+
self.affected_packages.add(*packages)
156157

157158
def add_cve(self, cve_id):
158159
""" Add a CVE to an Erratum object

packages/utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,22 @@ def get_matching_packages(name, epoch, version, release, p_type, arch=None):
277277
return packages
278278

279279

280+
def get_matching_packages_q(name, q, p_type, arch=None):
281+
""" Get packages matching a compound Q filter for batch version lookups.
282+
Returns the matching packages or an empty queryset.
283+
"""
284+
try:
285+
package_name = PackageName.objects.get(name=name)
286+
except PackageName.DoesNotExist:
287+
return Package.objects.none()
288+
base_filter = {'name': package_name, 'packagetype': p_type}
289+
if arch:
290+
if not isinstance(arch, PackageArchitecture):
291+
arch, _ = PackageArchitecture.objects.get_or_create(name=arch)
292+
base_filter['arch'] = arch
293+
return Package.objects.filter(q, **base_filter)
294+
295+
280296
def clean_packageupdates():
281297
""" Removes PackageUpdate objects that are no longer linked to any hosts
282298
"""

0 commit comments

Comments
 (0)