@@ -75,8 +75,8 @@ type Config struct {
7575}
7676
7777type StagingRequest struct {
78- // JDP staging request ID
79- Id int
78+ // JDP staging request IDs (batched because the endpoint has a maximum limit)
79+ Ids [] int
8080 // time of staging request (for purging)
8181 Time time.Time
8282}
@@ -173,20 +173,13 @@ func (db *Database) Search(orcid string, params databases.SearchParameters) (dat
173173 }, err
174174}
175175
176- func (db * Database ) Descriptors (orcid string , fileIds []string ) ([]map [string ]any , error ) {
177- // strip the "JDP:" prefix from our files and create a mapping from IDs to
178- // their original order so we can hand back metadata accordingly
176+ func (db * Database ) fetchDescriptors (orcid string , fileIds []string , batchSize int ) ([]map [string ]any , error ) {
177+ // strip the "JDP:" prefix from our files
179178 strippedFileIds := make ([]string , len (fileIds ))
180- indexForId := make (map [string ]int )
181179 for i , fileId := range fileIds {
182180 strippedFileIds [i ] = strings .TrimPrefix (fileId , "JDP:" )
183- indexForId [strippedFileIds [i ]] = i
184181 }
185182
186- // NOTE: the JDP search/by_file_ids/ endpoint (unofficial, undocumented!) only seems to
187- // NOTE: accept around 50 file IDs at a time, so we have to batch our requests
188-
189- batchSize := 50
190183 numBatches := len (strippedFileIds ) / batchSize
191184 if numBatches * batchSize < len (strippedFileIds ) {
192185 numBatches ++
@@ -216,33 +209,60 @@ func (db *Database) Descriptors(orcid string, fileIds []string) ([]map[string]an
216209 return nil , err
217210 }
218211
219- // get a de-duped list of descriptors
212+ // get a de-duped list of descriptors (with JDP: prefixes reinstated on IDs)
220213 batchDescriptors , err := descriptorsFromResponseBody (body , nil )
221214 if err != nil {
222215 return nil , err
223216 }
224217 descriptors = append (descriptors , batchDescriptors ... )
225218 }
219+ return descriptors , nil
220+ }
221+
222+ func (db * Database ) Descriptors (orcid string , fileIds []string ) ([]map [string ]any , error ) {
223+ // NOTE: The JDP search/by_file_ids/ endpoint (unofficial, undocumented!) only seems to
224+ // NOTE: accept around 50 file IDs at a time, so we have to batch our requests.
225+ descriptors , err := db .fetchDescriptors (orcid , fileIds , 50 )
226+ if err != nil {
227+ return nil , err
228+ }
226229
227- // reorder the descriptors to match that of the requested file IDs, and track file IDs that aren't
228- // matched to descriptors
229230 descriptorsByFileId := make (map [string ]map [string ]any )
230231 for _ , descriptor := range descriptors {
231232 descriptorsByFileId [descriptor ["id" ].(string )] = descriptor
232233 }
233234
234- // if any file IDs don't have corresponding descriptors, find out which ones and issue an error
235+ // NOTE: Evidently sometimes the search/by_file_ids endpoint doesn't return all of the
236+ // NOTE: relevant information (???), so we make a list of missing descriptors and attempt
237+ // NOTE: to fetch them again. If that attempt fails, we emit an error.
235238 if len (descriptorsByFileId ) < len (fileIds ) {
236- missingResources := make ([]string , 0 )
239+ missingFileIds := make ([]string , 0 )
237240 for _ , fileId := range fileIds {
238241 if _ , found := descriptorsByFileId [fileId ]; ! found {
239- missingResources = append (missingResources , fileId )
242+ missingFileIds = append (missingFileIds , fileId )
243+ }
244+ }
245+ if recoveredDescriptors , err := db .fetchDescriptors (orcid , missingFileIds , 50 ); err == nil {
246+ for _ , descriptor := range recoveredDescriptors {
247+ descriptorsByFileId [descriptor ["id" ].(string )] = descriptor
248+ }
249+ if len (recoveredDescriptors ) < len (missingFileIds ) { // didn't get them all!
250+ missingFileIds = make ([]string , 0 )
251+ for _ , fileId := range fileIds {
252+ if _ , found := descriptorsByFileId [fileId ]; ! found {
253+ missingFileIds = append (missingFileIds , fileId )
254+ }
255+ }
256+ } else {
257+ // got 'em!
258+ missingFileIds = nil
240259 }
241260 }
242- if len (missingResources ) > 0 {
261+ if len (missingFileIds ) > 0 {
262+ slices .Sort (missingFileIds )
243263 return nil , & databases.ResourcesNotFoundError {
244264 Database : "JDP" ,
245- ResourceIds : missingResources ,
265+ ResourceIds : missingFileIds ,
246266 }
247267 }
248268 }
@@ -258,9 +278,7 @@ func (db *Database) EndpointNames() []string {
258278 return []string {db .EndpointName }
259279}
260280
261- func (db * Database ) StageFiles (orcid string , fileIds []string ) (uuid.UUID , error ) {
262- var xferId uuid.UUID
263-
281+ func (db * Database ) requestArchivedFiles (orcid string , fileIds []string , batchSize int ) ([]int , error ) {
264282 // construct a POST request to restore archived files with the given IDs
265283 type RestoreRequest struct {
266284 Ids []string `json:"ids"`
@@ -277,72 +295,116 @@ func (db *Database) StageFiles(orcid string, fileIds []string) (uuid.UUID, error
277295 }
278296 }
279297
280- data , err := json .Marshal (RestoreRequest {
281- Ids : fileIdsWithoutPrefix ,
282- SendEmail : false ,
283- ApiVersion : "2" ,
284- IncludePrivateData : 1 , // we need this just in case!
285- })
286- if err != nil {
287- return xferId , err
298+ numBatches := len (fileIds ) / batchSize
299+ if numBatches * batchSize < len (fileIds ) {
300+ numBatches += 1
288301 }
289302
290- // NOTE: The slash in the resource is all-important for POST requests to
291- // NOTE: the JDP!!
292- body , err := db .post ("request_archived_files/" , orcid , bytes .NewReader (data ))
293- if err != nil {
294- switch e := err .(type ) {
295- case * databases.ResourcesNotFoundError :
296- e .ResourceIds = fileIds
303+ requestIds := make ([]int , numBatches )
304+ for i := range numBatches {
305+ begin := i * batchSize
306+ end := min ((i + 1 )* batchSize , len (fileIdsWithoutPrefix ))
307+ data , err := json .Marshal (RestoreRequest {
308+ Ids : fileIdsWithoutPrefix [begin :end ],
309+ SendEmail : false ,
310+ ApiVersion : "2" ,
311+ IncludePrivateData : 1 , // we need this just in case!
312+ })
313+ if err != nil {
314+ return nil , err
315+ }
316+
317+ // NOTE: The slash in the resource is all-important for POST requests to the JDP!!
318+ body , err := db .post ("request_archived_files/" , orcid , bytes .NewReader (data ))
319+ if err != nil {
320+ switch e := err .(type ) {
321+ case * databases.ResourcesNotFoundError :
322+ e .ResourceIds = fileIds
323+ }
324+ return nil , err
325+ }
326+
327+ type RestoreResponse struct {
328+ RequestId int `json:"request_id"`
297329 }
298- return xferId , err
299- }
300330
301- type RestoreResponse struct {
302- RequestId int `json:"request_id"`
331+ var jdpResp RestoreResponse
332+ err = json .Unmarshal (body , & jdpResp )
333+ if err != nil {
334+ return nil , err
335+ }
336+ requestIds [i ] = jdpResp .RequestId
303337 }
304338
305- var jdpResp RestoreResponse
306- err = json .Unmarshal (body , & jdpResp )
339+ return requestIds , nil
340+ }
341+
342+ func (db * Database ) StageFiles (orcid string , fileIds []string ) (uuid.UUID , error ) {
343+ var xferId uuid.UUID
344+
345+ // NOTE: the relevant endpoint seems to return a 404 whenever it gets too many file IDs,
346+ // NOTE: so we batch requests in sets of 1000
347+ requestIds , err := db .requestArchivedFiles (orcid , fileIds , 1000 )
307348 if err != nil {
308349 return xferId , err
309350 }
310- slog .Debug (fmt .Sprintf ("Requested %d archived files from JDP (request ID: %d)" ,
311- len (fileIds ), jdpResp .RequestId ))
351+
352+ slog .Debug (fmt .Sprintf ("Requested %d archived files from JDP (request IDs: %v)" ,
353+ len (fileIds ), requestIds ))
312354 xferId = uuid .New ()
313355 db .StagingRequests [xferId ] = StagingRequest {
314- Id : jdpResp . RequestId ,
356+ Ids : requestIds ,
315357 Time : time .Now (),
316358 }
317359 return xferId , err
318360}
319361
320362func (db * Database ) StagingStatus (id uuid.UUID ) (databases.StagingStatus , error ) {
363+ statusForString := map [string ]databases.StagingStatus {
364+ "new" : databases .StagingStatusActive ,
365+ "pending" : databases .StagingStatusActive ,
366+ "ready" : databases .StagingStatusSucceeded ,
367+ "failed" : databases .StagingStatusFailed ,
368+ }
321369 db .pruneStagingRequests ()
322370 if request , found := db .StagingRequests [id ]; found {
323- resource := fmt .Sprintf ("request_archived_files/requests/%d" , request .Id )
324- body , err := db .get (resource , url.Values {})
325- if err != nil {
326- return databases .StagingStatusUnknown , err
327- }
328- type JDPResult struct {
329- Status string `json:"status"` // "new", "pending", or "ready"
330- }
331- var jdpResult JDPResult
332- err = json .Unmarshal (body , & jdpResult )
333- if err != nil {
334- return databases .StagingStatusUnknown , err
335- }
336- statusForString := map [string ]databases.StagingStatus {
337- "new" : databases .StagingStatusActive ,
338- "pending" : databases .StagingStatusActive ,
339- "ready" : databases .StagingStatusSucceeded ,
340- }
341- if status , ok := statusForString [jdpResult .Status ]; ok {
342- slog .Debug (fmt .Sprintf ("Queried JDP for staging status of transfer with staging ID %s (request ID: %d): %s" , id , request .Id , jdpResult .Status ))
343- return status , nil
371+ var status databases.StagingStatus
372+ var statusStr string
373+ for _ , requestId := range request .Ids {
374+ resource := fmt .Sprintf ("request_archived_files/requests/%d" , requestId )
375+ body , err := db .get (resource , url.Values {})
376+ if err != nil {
377+ return databases .StagingStatusUnknown , err
378+ }
379+ type JDPResult struct {
380+ Status string `json:"status"` // "new", "pending", "ready", or "failed"
381+ }
382+ var jdpResult JDPResult
383+ err = json .Unmarshal (body , & jdpResult )
384+ if err != nil {
385+ return databases .StagingStatusUnknown , err
386+ }
387+ if requestStatus , ok := statusForString [jdpResult .Status ]; ok {
388+ if status == databases .StagingStatusUnknown { // first status encountered
389+ status = requestStatus
390+ statusStr = jdpResult .Status
391+ } else {
392+ if requestStatus != status { // status update
393+ if requestStatus != databases .StagingStatusSucceeded {
394+ status = requestStatus
395+ statusStr = jdpResult .Status
396+ }
397+ }
398+ }
399+ if status == databases .StagingStatusFailed { // one failure sinks them all
400+ break
401+ }
402+ } else {
403+ return databases .StagingStatusUnknown , fmt .Errorf ("unrecognized JDP staging status string: %s" , jdpResult .Status )
404+ }
344405 }
345- return databases .StagingStatusUnknown , fmt .Errorf ("unrecognized JDP staging status string: %s" , jdpResult .Status )
406+ slog .Debug (fmt .Sprintf ("Queried JDP for staging status of transfer with staging ID %s (request IDs: %v): %s" , id , request .Ids , statusStr ))
407+ return status , nil
346408 } else {
347409 slog .Info (fmt .Sprintf ("No staging request found for transfer with staging ID %s" , id .String ()))
348410 return databases .StagingStatusUnknown , nil
@@ -660,10 +722,6 @@ func (db *Database) post(resource, orcid string, body io.Reader) ([]byte, error)
660722 case 200 , 201 , 204 :
661723 defer resp .Body .Close ()
662724 return io .ReadAll (resp .Body )
663- case 404 :
664- return nil , & databases.ResourcesNotFoundError {
665- Database : "JDP" ,
666- }
667725 case 503 :
668726 return nil , & databases.UnavailableError {
669727 Database : "jdp" ,
@@ -855,7 +913,7 @@ func (db *Database) pruneStagingRequests() {
855913 for uuid , request := range db .StagingRequests {
856914 requestAge := time .Since (request .Time )
857915 if requestAge > db .DeleteAfter {
858- slog .Debug (fmt .Sprintf ("Pruning staging request with staging ID %s (request ID : %d ): age (%s) exceeds limit (%s)" , uuid .String (), request .Id , requestAge .String (), db .DeleteAfter .String ()))
916+ slog .Debug (fmt .Sprintf ("Pruning staging request with staging ID %s (request IDs : %v ): age (%s) exceeds limit (%s)" , uuid .String (), request .Ids , requestAge .String (), db .DeleteAfter .String ()))
859917 delete (db .StagingRequests , uuid )
860918 }
861919 }
0 commit comments