@@ -23,6 +23,7 @@ type packageIndex struct {
2323
2424func 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+ }
0 commit comments