Skip to content

Commit f2238ce

Browse files
Merge pull request #676 from chuguoxipo/develop
Initial commit for redrive upload API - implementation & unit tests
2 parents 0329c3d + f5f605d commit f2238ce

File tree

10 files changed

+431
-4
lines changed

10 files changed

+431
-4
lines changed

src/main/java/org/sagebionetworks/bridge/models/upload/UploadCompletionClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ public enum UploadCompletionClient {
1414
* Upload has been completed by a worker process that listens for the addition
1515
* of upload files to the file upload bucket on S3.
1616
*/
17-
S3_WORKER
17+
S3_WORKER,
18+
REDRIVE
1819
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.sagebionetworks.bridge.models.upload;
2+
3+
import java.util.List;
4+
import com.fasterxml.jackson.annotation.JsonCreator;
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
import com.google.common.collect.ImmutableList;
7+
8+
import org.sagebionetworks.bridge.models.BridgeEntity;
9+
10+
public class UploadRedriveList implements BridgeEntity {
11+
private final List<String> uploadIds;
12+
13+
@JsonCreator
14+
public UploadRedriveList(@JsonProperty("uploadIds") List<String> uploadIds) {
15+
this.uploadIds = uploadIds;
16+
}
17+
public List<String> getUploadIds() {
18+
return (uploadIds == null) ? ImmutableList.of() : uploadIds;
19+
}
20+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.sagebionetworks.bridge.models.worker;
2+
3+
/** Worker request to redrive uploads . */
4+
public class UploadRedriveWorkerRequest {
5+
private String s3Bucket;
6+
private String s3Key;
7+
private String redriveTypeStr;
8+
9+
public String getS3Bucket() {
10+
return s3Bucket;
11+
}
12+
13+
public String getS3Key() {
14+
return s3Key;
15+
}
16+
17+
public String getRedriveTypeStr() {
18+
return redriveTypeStr;
19+
}
20+
21+
public void setS3Bucket(String s3Bucket) {
22+
this.s3Bucket = s3Bucket;
23+
}
24+
25+
public void setS3Key(String s3Key) {
26+
this.s3Key = s3Key;
27+
}
28+
29+
public void setRedriveTypeStr(String redriveTypeStr) {
30+
this.redriveTypeStr = redriveTypeStr;
31+
}
32+
}

src/main/java/org/sagebionetworks/bridge/services/UploadService.java

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
import static org.sagebionetworks.bridge.BridgeConstants.API_APP_ID;
1313
import static org.sagebionetworks.bridge.BridgeConstants.API_DEFAULT_PAGE_SIZE;
1414
import static org.sagebionetworks.bridge.BridgeConstants.CANNOT_BE_BLANK;
15+
import static org.sagebionetworks.bridge.BridgeUtils.COMMA_SPACE_JOINER;
1516

17+
import java.io.IOException;
1618
import java.net.URL;
1719
import java.util.ArrayList;
1820
import java.util.Date;
@@ -30,14 +32,20 @@
3032
import com.amazonaws.services.s3.model.AmazonS3Exception;
3133
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
3234
import com.amazonaws.services.s3.model.ObjectMetadata;
35+
import com.amazonaws.services.sqs.AmazonSQS;
36+
import com.amazonaws.services.sqs.model.SendMessageResult;
3337
import com.fasterxml.jackson.core.JsonProcessingException;
3438
import com.fasterxml.jackson.databind.JsonNode;
39+
import com.fasterxml.jackson.databind.ObjectMapper;
3540
import com.fasterxml.jackson.databind.node.ObjectNode;
3641
import com.google.common.base.Stopwatch;
3742
import com.google.common.base.Strings;
3843
import com.google.common.collect.ImmutableSet;
3944
import org.joda.time.DateTime;
4045
import org.joda.time.DateTimeZone;
46+
47+
import org.sagebionetworks.bridge.BridgeConstants;
48+
import org.sagebionetworks.bridge.dao.HealthCodeDao;
4149
import org.sagebionetworks.bridge.exceptions.EntityNotFoundException;
4250
import org.sagebionetworks.bridge.models.PagedResourceList;
4351
import org.sagebionetworks.bridge.models.accounts.Account;
@@ -63,7 +71,11 @@
6371
import org.sagebionetworks.bridge.models.apps.App;
6472
import org.sagebionetworks.bridge.models.healthdata.HealthDataRecordEx3;
6573
import org.sagebionetworks.bridge.models.schedules2.timelines.TimelineMetadataView;
74+
import org.sagebionetworks.bridge.models.upload.UploadRedriveList;
6675
import org.sagebionetworks.bridge.models.upload.UploadViewEx3;
76+
import org.sagebionetworks.bridge.models.worker.UploadRedriveWorkerRequest;
77+
import org.sagebionetworks.bridge.models.worker.WorkerRequest;
78+
import org.sagebionetworks.bridge.s3.S3Helper;
6779
import org.sagebionetworks.bridge.time.DateUtils;
6880
import org.sagebionetworks.bridge.models.ForwardCursorPagedResourceList;
6981
import org.sagebionetworks.bridge.models.accounts.StudyParticipant;
@@ -88,9 +100,12 @@ public class UploadService {
88100

89101
// package-scoped to be available in unit tests
90102
static final String CONFIG_KEY_UPLOAD_BUCKET = "upload.bucket";
103+
static final String CONFIG_KEY_BACKFILL_BUCKET = "backfill.bucket";
104+
static final String REDRIVE_UPLOAD_S3_KEY_PREFIX = "redrive-upload-id-";
91105
static final String METADATA_KEY_EVENT_TIMESTAMP = "eventTimestamp";
92106
static final String METADATA_KEY_INSTANCE_GUID = "instanceGuid";
93107
static final String METADATA_KEY_STARTED_ON = "startedOn";
108+
static final String WORKER_NAME_UPLOAD_REDRIVE = "UploadRedriveWorker";
94109

95110
private AccountService accountService;
96111
private AdherenceService adherenceService;
@@ -103,9 +118,15 @@ public class UploadService {
103118
private Schedule2Service schedule2Service;
104119
private StudyService studyService;
105120
private String uploadBucket;
121+
private String redriveUploadBucket;
106122
private UploadDao uploadDao;
107123
private UploadDedupeDao uploadDedupeDao;
108124
private UploadValidationService uploadValidationService;
125+
private HealthCodeDao healthCodeDao;
126+
private String workerQueueUrl;
127+
private AmazonSQS sqsClient;
128+
private S3Helper s3Helper;
129+
private BridgeConfig config;
109130

110131
// These parameters can be overriden to facilitate testing.
111132
// By default, we sleep 5 seconds, including right at the start and end. This means on our 7th iteration,
@@ -136,7 +157,9 @@ public final void setExporter3Service(Exporter3Service exporter3Service) {
136157
/** Sets parameters from the specified Bridge config. */
137158
@Autowired
138159
final void setConfig(BridgeConfig config) {
160+
this.config = config;
139161
uploadBucket = config.getProperty(CONFIG_KEY_UPLOAD_BUCKET);
162+
redriveUploadBucket = config.getProperty(CONFIG_KEY_BACKFILL_BUCKET);
140163
}
141164

142165
/**
@@ -189,6 +212,21 @@ final void setUploadValidationService(UploadValidationService uploadValidationSe
189212
this.uploadValidationService = uploadValidationService;
190213
}
191214

215+
@Autowired
216+
final void setHealthCodeDao(HealthCodeDao healthCodeDao) {
217+
this.healthCodeDao = healthCodeDao;
218+
}
219+
220+
@Autowired
221+
final void setSqsClient(AmazonSQS sqsClient) {
222+
this.sqsClient = sqsClient;
223+
}
224+
225+
@Resource(name = "s3Helper")
226+
public final void setS3Helper(S3Helper s3Helper) {
227+
this.s3Helper = s3Helper;
228+
}
229+
192230
/**
193231
* Number of iterations while polling for validation status before we time out. This is used primarily by tests to
194232
* reduce the amount of wait time during tests.
@@ -527,6 +565,88 @@ public void uploadComplete(String appId, UploadCompletionClient completedBy, Upl
527565
// Save uploadedOn date and uploadId to related adherence records.
528566
updateAdherenceWithUploadInfo(appId, upload);
529567
}
568+
569+
public void redriveUpload(UploadRedriveList redriveList) throws IOException {
570+
checkNotNull(redriveList);
571+
572+
if (redriveList.getUploadIds().isEmpty()) {
573+
throw new BadRequestException("No upload ids submitted for redrive");
574+
}
575+
576+
int redriveSize = redriveList.getUploadIds().size();
577+
578+
logger.info("Redrive uploads" + " with " + redriveSize + " upload ids");
579+
580+
// Redrive upload.
581+
if (redriveSize <= 10) {
582+
redriveSmallAmountOfUploads(redriveList.getUploadIds());
583+
} else {
584+
redriveLargeAmountOfUploads(redriveList.getUploadIds());
585+
}
586+
}
587+
588+
private void redriveSmallAmountOfUploads(List<String> uploadIds) throws JsonProcessingException {
589+
for (String uploadId : uploadIds) {
590+
Upload upload = getUpload(uploadId);
591+
String appId = upload.getAppId();
592+
if (appId == null) {
593+
appId = healthCodeDao.getAppId(upload.getHealthCode());
594+
}
595+
uploadComplete(appId, UploadCompletionClient.REDRIVE, upload, true);
596+
}
597+
598+
for (String uploadId: uploadIds) {
599+
UploadValidationStatus validationStatus = pollUploadValidationStatusUntilComplete(uploadId);
600+
if (validationStatus.getStatus() != UploadStatus.SUCCEEDED) {
601+
logErrorMessage(uploadId, validationStatus);
602+
}
603+
}
604+
}
605+
606+
// For unit test.
607+
protected void logErrorMessage(String uploadId, UploadValidationStatus validationStatus) {
608+
logger.error("Redrive failed for uploadId=" + uploadId + ": " + COMMA_SPACE_JOINER.join(
609+
validationStatus.getMessageList()));
610+
}
611+
612+
private void redriveLargeAmountOfUploads(List<String> uploadIds) throws IOException {
613+
// set up s3 Key.
614+
String currentTime = String.valueOf(DateUtils.getCurrentDateTime().getMillis());
615+
String s3Key = REDRIVE_UPLOAD_S3_KEY_PREFIX + currentTime;
616+
617+
// Upload file to S3 bucket
618+
s3Helper.writeLinesToS3(redriveUploadBucket, s3Key, uploadIds);
619+
620+
// write Json message to sqs
621+
// 1. Create request.
622+
UploadRedriveWorkerRequest uploadRedriveWorkerRequest = new UploadRedriveWorkerRequest();
623+
uploadRedriveWorkerRequest.setS3Key(s3Key);
624+
uploadRedriveWorkerRequest.setS3Bucket(redriveUploadBucket);
625+
uploadRedriveWorkerRequest.setRedriveTypeStr("upload_id");
626+
627+
WorkerRequest workerRequest = new WorkerRequest();
628+
workerRequest.setService(WORKER_NAME_UPLOAD_REDRIVE);
629+
workerRequest.setBody(uploadRedriveWorkerRequest);
630+
631+
// Convert request to JSON.
632+
ObjectMapper objectMapper = BridgeObjectMapper.get();
633+
String requestJson;
634+
try {
635+
requestJson = objectMapper.writeValueAsString(workerRequest);
636+
} catch (JsonProcessingException ex) {
637+
// This should never happen, but catch and re-throw for code hygiene.
638+
throw new BridgeServiceException("Error creating upload redrive request for S3 file " + s3Key,
639+
ex);
640+
}
641+
642+
// 2. Send to SQS.
643+
// Note: SqsInitializer runs after Spring, so we need to grab the queue URL dynamically.
644+
workerQueueUrl = config.getProperty(BridgeConstants.CONFIG_KEY_WORKER_SQS_URL);
645+
646+
SendMessageResult sqsResult = sqsClient.sendMessage(workerQueueUrl, requestJson);
647+
logger.info("Sent redrive upload request for file " + s3Key +
648+
sqsResult.getMessageId());
649+
}
530650

531651
public void deleteUploadsForHealthCode(String healthCode) {
532652
checkArgument(isNotBlank(healthCode));

src/main/java/org/sagebionetworks/bridge/spring/controllers/UploadController.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import static org.sagebionetworks.bridge.Roles.WORKER;
88
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
99

10+
import java.io.IOException;
1011
import java.util.EnumSet;
1112
import java.util.Optional;
1213

@@ -20,16 +21,19 @@
2021
import org.springframework.web.bind.annotation.PostMapping;
2122
import org.springframework.web.bind.annotation.RequestParam;
2223
import org.springframework.web.bind.annotation.RestController;
24+
2325
import org.sagebionetworks.bridge.Roles;
2426
import org.sagebionetworks.bridge.dao.HealthCodeDao;
2527
import org.sagebionetworks.bridge.exceptions.EntityNotFoundException;
2628
import org.sagebionetworks.bridge.exceptions.UnauthorizedException;
2729
import org.sagebionetworks.bridge.models.Metrics;
2830
import org.sagebionetworks.bridge.models.RequestInfo;
31+
import org.sagebionetworks.bridge.models.StatusMessage;
2932
import org.sagebionetworks.bridge.models.accounts.UserSession;
3033
import org.sagebionetworks.bridge.models.healthdata.HealthDataRecord;
3134
import org.sagebionetworks.bridge.models.upload.Upload;
3235
import org.sagebionetworks.bridge.models.upload.UploadCompletionClient;
36+
import org.sagebionetworks.bridge.models.upload.UploadRedriveList;
3337
import org.sagebionetworks.bridge.models.upload.UploadRequest;
3438
import org.sagebionetworks.bridge.models.upload.UploadSession;
3539
import org.sagebionetworks.bridge.models.upload.UploadValidationStatus;
@@ -49,6 +53,8 @@ public class UploadController extends BaseController {
4953

5054
private HealthCodeDao healthCodeDao;
5155

56+
static final StatusMessage REDRIVE_COMPLETE_MSG = new StatusMessage("Upload redrive completed.");
57+
5258
@Autowired
5359
final void setUploadService(UploadService uploadService) {
5460
this.uploadService = uploadService;
@@ -141,7 +147,7 @@ public String uploadComplete(@PathVariable String uploadId,
141147
if (appId == null) {
142148
appId = healthCodeDao.getAppId(upload.getHealthCode());
143149
}
144-
uploadCompletionClient = UploadCompletionClient.S3_WORKER;
150+
uploadCompletionClient = redrive? UploadCompletionClient.REDRIVE : UploadCompletionClient.S3_WORKER;
145151
} else {
146152
// Or, the consented user that originally made the upload request. Check that health codes match.
147153
// Do not need to look up the app.
@@ -167,6 +173,16 @@ public String uploadComplete(@PathVariable String uploadId,
167173
// Upload validation status may contain the health data record. Use the filter to filter out health code.
168174
return HealthDataRecord.PUBLIC_RECORD_WRITER.writeValueAsString(validationStatus);
169175
}
176+
177+
@PostMapping("/v3/uploads/redrive")
178+
public StatusMessage redriveUploads() throws IOException {
179+
getAuthenticatedSession(Roles.SUPERADMIN);
180+
181+
UploadRedriveList redriveList = parseJson(UploadRedriveList.class);
182+
uploadService.redriveUpload(redriveList);
183+
184+
return REDRIVE_COMPLETE_MSG;
185+
}
170186

171187
@GetMapping("/v3/uploads/{uploadId}")
172188
public UploadView getUpload(@PathVariable String uploadId) {

src/main/resources/BridgeServer2.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ attachment.bucket = org-sagebridge-attachment-${bucket.suffix}
109109
# Exporter 3 Health Data buckets
110110
health.data.bucket.raw = org-sagebridge-rawhealthdata-${bucket.suffix}
111111

112+
# Backfill buckets
113+
local.backfill.bucket = org-sagebridge-backfill-devlocal
114+
dev.backfill.bucket = org-sagebridge-backfill-devdevelop
115+
uat.backfill.bucket = org-sagebridge-backfill-devstaging
116+
prod.backfill.bucket = org-sagebridge-backfill-prod
117+
112118
# Upload CMS certificate information
113119
upload.cms.certificate.country = US
114120
upload.cms.certificate.state = WA

src/test/java/org/sagebionetworks/bridge/models/schedules2/adherence/AdherenceRecordListTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public void canSerialize() throws Exception {
2020
AdherenceRecordList list = new AdherenceRecordList(ImmutableList.of(rec1, rec2));
2121

2222
JsonNode node = BridgeObjectMapper.get().valueToTree(list);
23+
2324
assertEquals(node.get("records").size(), 2);
2425
// just verify these are adherence records, which we test separately
2526
assertEquals(node.get("records").get(0).get("clientTimeZone").textValue(), "America/Los_Angeles");
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.sagebionetworks.bridge.models.upload;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.google.common.collect.ImmutableList;
5+
import org.testng.annotations.Test;
6+
7+
import static org.testng.Assert.assertEquals;
8+
import org.sagebionetworks.bridge.json.BridgeObjectMapper;
9+
10+
public class UploadRedriveListTest {
11+
private static final String UPLOAD_ID_1 = "upload1";
12+
private static final String UPLOAD_ID_2 = "upload2";
13+
14+
@Test
15+
public void canSerialize() throws Exception {
16+
UploadRedriveList list = new UploadRedriveList(ImmutableList.of(UPLOAD_ID_1, UPLOAD_ID_2));
17+
18+
JsonNode node = BridgeObjectMapper.get().valueToTree(list);
19+
20+
assertEquals(node.get("uploadIds").size(), 2);
21+
// just verify these are adherence records, which we test separately
22+
assertEquals(node.get("uploadIds").get(0).textValue(), UPLOAD_ID_1);
23+
assertEquals(node.get("uploadIds").get(1).textValue(), UPLOAD_ID_2);
24+
assertEquals(node.get("type").textValue(), "UploadRedriveList");
25+
26+
UploadRedriveList deser = BridgeObjectMapper.get().readValue(node.toString(), UploadRedriveList.class);
27+
assertEquals(deser.getUploadIds().size(), 2);
28+
assertEquals(deser.getUploadIds().get(0), UPLOAD_ID_1);
29+
assertEquals(deser.getUploadIds().get(1), UPLOAD_ID_2);
30+
}
31+
32+
@Test
33+
public void nullList() {
34+
UploadRedriveList list = new UploadRedriveList(null);
35+
assertEquals(list.getUploadIds(), ImmutableList.of());
36+
}
37+
}

0 commit comments

Comments
 (0)