Skip to content

Commit d8dc0f1

Browse files
committed
Merge branch 'jdp-missing-files' into add-jdp-datasets-search-param
2 parents 360b13d + d660a88 commit d8dc0f1

166 files changed

Lines changed: 4349 additions & 4944 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

databases/jdp/database.go

Lines changed: 131 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ type Config struct {
7575
}
7676

7777
type 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

320362
func (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
}

etc/irods/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ requires-python = ">=3.10"
66
dependencies = [
77
"boto3>=1.41.5",
88
"python-irodsclient>=3.2.0",
9+
"urllib3>=2.7.0",
910
]

etc/irods/uv.lock

Lines changed: 5 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ toolchain go1.24.0
66

77
require (
88
github.com/StalkR/hsts v1.0.3
9-
github.com/aws/aws-sdk-go-v2 v1.39.6
9+
github.com/aws/aws-sdk-go-v2 v1.41.5
1010
github.com/aws/aws-sdk-go-v2/config v1.31.17
1111
github.com/aws/aws-sdk-go-v2/credentials v1.18.21
1212
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4
13-
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0
13+
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3
1414
github.com/danielgtaylor/huma/v2 v2.27.0
1515
github.com/fernet/fernet-go v0.0.0-20240119011108-303da6aec611
1616
github.com/frictionlessdata/datapackage-go v1.0.4
@@ -26,14 +26,14 @@ require (
2626
require (
2727
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
2828
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
29-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
30-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
29+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
30+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
3131
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
32-
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect
33-
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
34-
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect
35-
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
36-
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
32+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
33+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
34+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
35+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
36+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
3737
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
3838
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
3939
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect

0 commit comments

Comments
 (0)