Skip to content

Commit 7fb9449

Browse files
committed
jsonblob: add an iterator interface
This has the goal of reducing peak memory usage. Signed-off-by: Hank Donnay <hdonnay@redhat.com> Change-Id: I387d9289aad128724e57b9cebd213a3c6a6a6964
1 parent 301ff9a commit 7fb9449

2 files changed

Lines changed: 133 additions & 8 deletions

File tree

libvuln/jsonblob/jsonblob.go

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"errors"
1010
"fmt"
1111
"io"
12+
"iter"
1213
"os"
1314
"sort"
1415
"sync"
@@ -45,29 +46,43 @@ type Store struct {
4546
}
4647

4748
// Load reads in all the records serialized in the provided [io.Reader].
49+
//
50+
// Deprecated: This just calls [NewLoader].
51+
//
52+
//go:fix inline
4853
func Load(ctx context.Context, r io.Reader) (*Loader, error) {
54+
return NewLoader(ctx, r)
55+
}
56+
57+
// NewLoader returns a loader configured to read the records serialized in the
58+
// provided [io.Reader].
59+
func NewLoader(ctx context.Context, r io.Reader) (*Loader, error) {
4960
l := Loader{
5061
dec: json.NewDecoder(r),
5162
cur: uuid.Nil,
5263
}
5364
return &l, nil
5465
}
5566

56-
// Loader is an iterator that returns a series of [Entry].
67+
// Loader is an iterator over serialized records.
5768
//
58-
// Users should call [*Loader.Next] until it reports false, then check for
59-
// errors via [*Loader.Err].
69+
// Users should consume one of the [Loader.Vulnerabilities] or
70+
// [Loader.Enrichments] iterators, then check for errors via [Loader.Err].
6071
type Loader struct {
61-
err error
62-
e *Entry
72+
err error
73+
dec *json.Decoder
74+
started bool
6375

64-
dec *json.Decoder
76+
// Used in the deprecated [Loader.Next]+[Loader.Entry] flow.
77+
e *Entry
6578
next *Entry
6679
de diskEntry
6780
cur uuid.UUID
6881
}
6982

7083
// Next reports whether there's an [Entry] to be processed.
84+
//
85+
// Deprecated: Use the [Loader.All] iterator.
7186
func (l *Loader) Next() bool {
7287
if l.err != nil {
7388
return false
@@ -113,10 +128,75 @@ func (l *Loader) Next() bool {
113128
}
114129

115130
// Entry returns the latest loaded [Entry].
131+
//
132+
// Deprecated: Use the [Loader.All] iterator.
116133
func (l *Loader) Entry() *Entry {
117134
return l.e
118135
}
119136

137+
// All returns an iterator-of-iterators yielding all entries in the Loader.
138+
//
139+
// The inner iterator returns exactly one of the two values.
140+
func (l *Loader) All() iter.Seq2[*Entry, iter.Seq2[*claircore.Vulnerability, *driver.EnrichmentRecord]] {
141+
if l.started {
142+
l.err = errors.Join(l.err, fmt.Errorf("attempted to re-use a Loader"))
143+
return noopIterator
144+
}
145+
146+
// These are shared across the two iterators:
147+
cur := uuid.Nil
148+
var de diskEntry
149+
150+
// The inner iterator decodes every entry after the first one, stopping
151+
// iteration and passing control to the outer iterator when the Ref
152+
// changes.
153+
inner := func(yield func(*claircore.Vulnerability, *driver.EnrichmentRecord) bool) {
154+
for ; l.err == nil && cur == de.Ref; l.err = l.dec.Decode(&de) {
155+
var v *claircore.Vulnerability
156+
var e *driver.EnrichmentRecord
157+
switch de.Kind {
158+
case driver.VulnerabilityKind:
159+
v = getVulnerability()
160+
l.err = json.Unmarshal(de.Vuln.buf, v)
161+
case driver.EnrichmentKind:
162+
e = getEnrichment()
163+
l.err = json.Unmarshal(de.Enrichment.buf, e)
164+
}
165+
if l.err != nil || !yield(v, e) {
166+
return
167+
}
168+
}
169+
}
170+
// Outer reads the first entry and handles when the Ref changes.
171+
outer := func(yield func(*Entry, iter.Seq2[*claircore.Vulnerability, *driver.EnrichmentRecord]) bool) {
172+
l.started = true
173+
174+
for l.err = l.dec.Decode(&de); l.err == nil; {
175+
ent := &Entry{
176+
CommonEntry: CommonEntry{
177+
Updater: de.Updater,
178+
Fingerprint: de.Fingerprint,
179+
Date: de.Date,
180+
Kind: de.Kind,
181+
},
182+
}
183+
cur = de.Ref
184+
if !yield(ent, inner) {
185+
return
186+
}
187+
for ; l.err == nil && cur == de.Ref; l.err = l.dec.Decode(&de) {
188+
// Skip entries if the inner iterator was not fully consumed.
189+
}
190+
}
191+
}
192+
193+
return outer
194+
}
195+
196+
// NoopIterator is an iterator that does nothing.
197+
func noopIterator(_ func(*Entry, iter.Seq2[*claircore.Vulnerability, *driver.EnrichmentRecord]) bool) {
198+
}
199+
120200
// Err is the latest encountered error.
121201
func (l *Loader) Err() error {
122202
// Don't report EOF as an error.
@@ -148,10 +228,10 @@ func (s *Store) Store(w io.Writer) error {
148228
shim := newBufShim(f)
149229
defer shim.Close()
150230
for range ct {
231+
e.Kind = k
151232
dent := diskEntry{
152233
CommonEntry: e,
153234
Ref: id,
154-
Kind: k,
155235
}
156236
switch k {
157237
case driver.EnrichmentKind:
@@ -237,6 +317,7 @@ type CommonEntry struct {
237317
Updater string
238318
Fingerprint driver.Fingerprint
239319
Date time.Time
320+
Kind driver.UpdateKind
240321
}
241322

242323
// DiskEntry is a single vulnerability or enrichment. It's made from unpacking an
@@ -249,7 +330,6 @@ type diskEntry struct {
249330
Ref uuid.UUID
250331
Vuln *bufShim `json:",omitempty"`
251332
Enrichment *bufShim `json:",omitempty"`
252-
Kind driver.UpdateKind
253333
}
254334

255335
// Entries returns a map containing all the Entries stored by calls to
@@ -477,6 +557,7 @@ func getBuf() []byte {
477557
}
478558
return b
479559
}
560+
480561
func putBuf(b []byte) {
481562
bufPool.Put(b)
482563
}

libvuln/jsonblob/pool.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package jsonblob
2+
3+
import (
4+
"sync"
5+
6+
"github.com/quay/claircore"
7+
"github.com/quay/claircore/libvuln/driver"
8+
)
9+
10+
var (
11+
vulnerability sync.Pool
12+
enrichment sync.Pool
13+
)
14+
15+
func getVulnerability() *claircore.Vulnerability {
16+
if v := vulnerability.Get(); v != nil {
17+
return v.(*claircore.Vulnerability)
18+
}
19+
return new(claircore.Vulnerability)
20+
}
21+
22+
// ReturnVulnerability can be used by callers to return
23+
// [claircore.Vulnerability] objects from [Loader.All] iterators to a common
24+
// pool.
25+
//
26+
// This may take some pressure off the garbage collector.
27+
func ReturnVulnerability(v *claircore.Vulnerability) {
28+
vulnerability.Put(v)
29+
}
30+
31+
func getEnrichment() *driver.EnrichmentRecord {
32+
if v := enrichment.Get(); v != nil {
33+
return v.(*driver.EnrichmentRecord)
34+
}
35+
return new(driver.EnrichmentRecord)
36+
}
37+
38+
// ReturnEnrichment can be used by callers to return [driver.EnrichmentRecord]
39+
// objects from [Loader.All] iterators to a common pool.
40+
//
41+
// This may take some pressure off the garbage collector.
42+
func ReturnEnrichment(e *driver.EnrichmentRecord) {
43+
enrichment.Put(e)
44+
}

0 commit comments

Comments
 (0)