Skip to content

Commit 203a984

Browse files
authored
Read packages from index in batch to insert them in the SQL database (#1326)
Updated the process to load the search index from the storage to read in batches all the packages. Each batch of packages is inserted directly in the database. This allows us to not keep all the databases from the storage index in memory.
1 parent 51c8913 commit 203a984

2 files changed

Lines changed: 134 additions & 24 deletions

File tree

internal/storage/index.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type packageIndex struct {
2323

2424
func loadSearchIndexAll(ctx context.Context, logger *zap.Logger, storageClient *storage.Client, bucketName, rootStoragePath string, aCursor cursor) (*packages.Packages, error) {
2525
span, ctx := apm.StartSpan(ctx, "LoadSearchIndexAll", "app")
26+
span.Context.SetLabel("load.method", "full")
2627
defer span.End()
2728

2829
indexFile := searchIndexAllFile
@@ -114,3 +115,112 @@ func LoadPackagesAndCursorFromIndex(ctx context.Context, logger *zap.Logger, sto
114115
}
115116
return anIndex, storageCursor.Current, nil
116117
}
118+
119+
func loadSearchIndexAllBatches(ctx context.Context, logger *zap.Logger, storageClient *storage.Client, bucketName, rootStoragePath string, aCursor cursor, batchSize int, process func(packages.Packages) error) error {
120+
span, ctx := apm.StartSpan(ctx, "LoadSearchIndexAll", "app")
121+
span.Context.SetLabel("load.method", "batches")
122+
span.Context.SetLabel("load.batch.size", batchSize)
123+
defer span.End()
124+
125+
indexFile := searchIndexAllFile
126+
127+
logger.Debug("load search-index-all index", zap.String("index.file", indexFile))
128+
129+
rootedIndexStoragePath := buildIndexStoragePath(rootStoragePath, aCursor, indexFile)
130+
objectReader, err := storageClient.Bucket(bucketName).Object(rootedIndexStoragePath).NewReader(ctx)
131+
if err != nil {
132+
return fmt.Errorf("can't read the index file (path: %s): %w", rootedIndexStoragePath, err)
133+
}
134+
defer objectReader.Close()
135+
136+
// Using a decoder here as tokenizer to parse the list of packages as a stream
137+
// instead of needing the whole document in memory at the same time. This helps
138+
// reducing memory usage.
139+
// Using `Unmarshal(doc, &sia)` would require to read the whole document.
140+
// Using `dec.Decode(&sia)` would also make the decoder to keep the whole document
141+
// in memory.
142+
// `jsoniter` seemed to be slightly faster, but to use more memory for our use case,
143+
// and we are looking to optimize for memory use.
144+
dec := json.NewDecoder(objectReader)
145+
count := 0
146+
packages := make(packages.Packages, 0, batchSize)
147+
for dec.More() {
148+
// Read everything till the "packages" key in the map.
149+
token, err := dec.Token()
150+
if err != nil {
151+
return fmt.Errorf("unexpected error while reading index file: %w", err)
152+
}
153+
if key, ok := token.(string); !ok || key != "packages" {
154+
continue
155+
}
156+
157+
// Read the opening array now.
158+
token, err = dec.Token()
159+
if err != nil {
160+
return fmt.Errorf("unexpected error while reading index file: %w", err)
161+
}
162+
if delim, ok := token.(json.Delim); !ok || delim != '[' {
163+
return fmt.Errorf("expected opening array, found %v", token)
164+
}
165+
166+
// Read the array of packages one by one.
167+
for dec.More() {
168+
var p packageIndex
169+
err = dec.Decode(&p)
170+
if err != nil {
171+
return fmt.Errorf("unexpected error parsing package from index file (token: %v): %w", token, err)
172+
}
173+
packages = append(packages, p.PackageManifest)
174+
count++
175+
176+
if count >= batchSize {
177+
err = process(packages)
178+
if err != nil {
179+
return fmt.Errorf("error processing batch of packages: %w", err)
180+
}
181+
count = 0
182+
packages = packages[:0] // Reset the slice to reuse the memory
183+
}
184+
}
185+
186+
// Read the closing array delimiter.
187+
token, err = dec.Token()
188+
if err != nil {
189+
return fmt.Errorf("unexpected error while reading index file: %w", err)
190+
}
191+
if delim, ok := token.(json.Delim); !ok || delim != ']' {
192+
return fmt.Errorf("expected closing array, found %v", token)
193+
}
194+
}
195+
if len(packages) > 0 {
196+
err = process(packages)
197+
if err != nil {
198+
return fmt.Errorf("error processing final batch of packages: %w", err)
199+
}
200+
}
201+
return nil
202+
}
203+
204+
func LoadPackagesAndCursorFromIndexBatches(ctx context.Context, logger *zap.Logger, storageClient *storage.Client, storageBucketInternal, currentCursor string, batchSize int, process func(packages.Packages) error) (string, error) {
205+
bucketName, rootStoragePath, err := extractBucketNameFromURL(storageBucketInternal)
206+
if err != nil {
207+
return "", fmt.Errorf("can't extract bucket name from URL (url: %s): %w", storageBucketInternal, err)
208+
}
209+
210+
storageCursor, err := loadCursor(ctx, logger, storageClient, bucketName, rootStoragePath)
211+
if err != nil {
212+
return "", fmt.Errorf("can't load latest cursor: %w", err)
213+
}
214+
215+
if storageCursor.Current == currentCursor {
216+
logger.Info("cursor is up-to-date", zap.String("cursor.current", currentCursor))
217+
return currentCursor, nil
218+
}
219+
logger.Info("cursor will be updated", zap.String("cursor.current", currentCursor), zap.String("cursor.next", storageCursor.Current))
220+
221+
err = loadSearchIndexAllBatches(ctx, logger, storageClient, bucketName, rootStoragePath, *storageCursor, batchSize, process)
222+
if err != nil {
223+
return "", fmt.Errorf("can't load the search-index-all index content: %w", err)
224+
}
225+
return storageCursor.Current, nil
226+
}

internal/storage/sqlindexer.go

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -199,45 +199,45 @@ func (i *SQLIndexer) updateIndex(ctx context.Context) error {
199199
metrics.StorageIndexerUpdateIndexDurationSeconds.Observe(time.Since(start).Seconds())
200200
}()
201201

202-
anIndex, currentCursor, err := LoadPackagesAndCursorFromIndex(ctx, i.logger, i.storageClient, i.options.PackageStorageBucketInternal, i.cursor)
202+
defer func() {
203+
startClean := time.Now()
204+
if err := i.cleanBackupDatabase(ctx); err != nil {
205+
i.logger.Error("Failed to clean backup database", zap.Error(err))
206+
}
207+
startCleanDuration := time.Since(startClean)
208+
i.logger.Debug("Cleaned backup database", zap.Duration("elapsed.time", time.Since(startClean)), zap.String("elapsed.time.human", startCleanDuration.String()))
209+
}()
210+
211+
numPackages := 0
212+
currentCursor, err := LoadPackagesAndCursorFromIndexBatches(ctx, i.logger, i.storageClient, i.options.PackageStorageBucketInternal, i.cursor, i.maxBulkAddBatch, func(pkgs packages.Packages) error {
213+
// This function is called for each batch of packages read from the index.
214+
startUpdate := time.Now()
215+
if err := i.updateDatabase(ctx, &pkgs); err != nil {
216+
return fmt.Errorf("failed to update database: %w", err)
217+
}
218+
startDuration := time.Since(startUpdate)
219+
numPackages += len(pkgs)
220+
i.logger.Debug("Filled database with a batch of packages", zap.Duration("elapsed.time", startDuration), zap.String("elapsed.time.human", startDuration.String()), zap.Int("num.packages", len(pkgs)))
221+
return nil
222+
})
203223
if err != nil {
204224
metrics.StorageIndexerUpdateIndexErrorsTotal.Inc()
205225
return fmt.Errorf("can't load the search-index-all index content: %w", err)
206226
}
207227
if i.cursor == currentCursor {
208228
return nil
209229
}
210-
if anIndex == nil {
211-
i.logger.Info("Downloaded new search-index-all index. No packages found.")
212-
return nil
213-
}
214-
i.logger.Info("Downloaded new search-index-all index", zap.String("index.packages.size", fmt.Sprintf("%d", len(*anIndex))))
215-
216-
i.logger.Info("Filling database")
217-
startUpdate := time.Now()
218-
err = i.updateDatabase(ctx, anIndex)
219-
if err != nil {
220-
return fmt.Errorf("failed to update database: %w", err)
221-
}
222-
startDuration := time.Since(startUpdate)
223-
i.logger.Info("Filled database with latest packages", zap.Duration("elapsed.time", startDuration), zap.String("elapsed.time.human", startDuration.String()))
230+
i.logger.Info("Downloaded new search-index-all index", zap.String("index.packages.size", fmt.Sprintf("%d", numPackages)))
224231

225232
startLock := time.Now()
226-
i.swapDatabases(ctx, currentCursor, len(*anIndex))
227-
i.logger.Info("Elapsed time in lock for updating index database", zap.Duration("lock.duration", time.Since(startLock)))
233+
i.swapDatabases(ctx, currentCursor, numPackages)
234+
i.logger.Debug("Elapsed time in lock for updating index database", zap.Duration("lock.duration", time.Since(startLock)))
228235

229236
if err != nil {
230237
metrics.StorageIndexerUpdateIndexErrorsTotal.Inc()
231238
return err
232239
}
233240

234-
startClean := time.Now()
235-
err = i.cleanBackupDatabase(ctx)
236-
if err != nil {
237-
return fmt.Errorf("failed to clean backup database: %w", err)
238-
}
239-
startCleanDuration := time.Since(startClean)
240-
i.logger.Info("Cleaned backup database", zap.Duration("elapsed.time", time.Since(startClean)), zap.String("elapsed.time.human", startCleanDuration.String()))
241241
return nil
242242
}
243243

0 commit comments

Comments
 (0)