Skip to content

Commit 0f33653

Browse files
committed
Handle concurrent run of PhraseTMS sync
Remove Phrase tags only they are older than 5 minutes, this way concurrent sync don't end up in a state where there are no tags, which break the "pull" logic. The time window needs to be higher than the time "push" takes, putting 5 mintues for now.
1 parent 043881f commit 0f33653

File tree

2 files changed

+85
-2
lines changed

2 files changed

+85
-2
lines changed

webapp/src/main/java/com/box/l10n/mojito/service/thirdparty/ThirdPartyTMSPhrase.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import com.phrase.client.model.TranslationKey;
2828
import io.micrometer.core.instrument.MeterRegistry;
2929
import io.micrometer.core.instrument.Timer;
30+
import java.time.Duration;
31+
import java.time.LocalDateTime;
3032
import java.time.ZonedDateTime;
3133
import java.time.format.DateTimeFormatter;
3234
import java.util.AbstractMap.SimpleEntry;
@@ -52,6 +54,7 @@ public class ThirdPartyTMSPhrase implements ThirdPartyTMS {
5254

5355
static final String TAG_PREFIX = "push_";
5456
static final String TAG_PREFIX_WITH_REPOSITORY = "push_%s";
57+
static final String TAG_DATE_FORMAT = "yyyy_MM_dd_HH_mm_ss_SSS";
5558
static final boolean NATIVE_CLIENT_DEFAULT_VALUE = false;
5659

5760
static Logger logger = LoggerFactory.getLogger(ThirdPartyTMSPhrase.class);
@@ -249,12 +252,22 @@ public void removeUnusedKeysAndTags(
249252
.filter(Objects::nonNull)
250253
.filter(tagName -> tagName.startsWith(TAG_PREFIX))
251254
.filter(tagName -> !allActiveTags.contains(tagName))
255+
// That's to handle concurrent sync and make sure a tag that was just pushed is not
256+
// deleted before the pull from the same sync is finished.
257+
// We don't prevent syncs to run concurrently
258+
.filter(tagName -> !areTagsWithin5Minutes(tagForUpload, tagName))
252259
.toList();
253260

254261
logger.info("Tags to delete: {}", pushTagsToDelete);
255262
phraseClient.deleteTags(projectId, pushTagsToDelete);
263+
logger.info("RemoveUnusedKeysAndTags took: {}", stopwatchRemoveUnusedKeysAndTags);
264+
}
256265

257-
logger.info("removeUnusedKeysAndTags took: {}", stopwatchRemoveUnusedKeysAndTags);
266+
static boolean areTagsWithin5Minutes(String tagName1, String tagName2) {
267+
return Duration.between(uploadTagToLocalDateTime(tagName2), uploadTagToLocalDateTime(tagName1))
268+
.abs()
269+
.getSeconds()
270+
< (60 * 5);
258271
}
259272

260273
private List<TextUnitDTO> getSourceTextUnitDTOs(
@@ -293,7 +306,7 @@ private List<TextUnitDTO> getSourceTextUnitDTOsPluralOnly(
293306

294307
public static String getTagForUpload(String repositoryName) {
295308
ZonedDateTime zonedDateTime = JSR310Migration.dateTimeNowInUTC();
296-
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy_MM_dd_HH_mm_ss_SSS");
309+
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(TAG_DATE_FORMAT);
297310
return normalizeTagName(
298311
"%s%s_%s_%s"
299312
.formatted(
@@ -303,6 +316,27 @@ public static String getTagForUpload(String repositoryName) {
303316
Math.abs(UUID.randomUUID().getLeastSignificantBits() % 1000)));
304317
}
305318

319+
public static LocalDateTime uploadTagToLocalDateTime(String tag) {
320+
321+
if (tag == null || !tag.contains("_")) {
322+
throw new IllegalArgumentException("Invalid tag format: " + tag);
323+
}
324+
325+
int dateEndIndex = tag.lastIndexOf('_'); // last part is a random number
326+
int dateStartIndex = dateEndIndex;
327+
328+
for (int i = 0; i < 7; i++) {
329+
dateStartIndex = tag.lastIndexOf('_', dateStartIndex - 1);
330+
if (dateStartIndex == -1) {
331+
throw new IllegalArgumentException("Invalid tag format: " + tag);
332+
}
333+
}
334+
335+
String dateTimePart = tag.substring(dateStartIndex + 1, dateEndIndex);
336+
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(TAG_DATE_FORMAT);
337+
return LocalDateTime.parse(dateTimePart, formatter);
338+
}
339+
306340
private static String getTagNamePrefixForRepository(String repositoryName) {
307341
return normalizeTagName(TAG_PREFIX_WITH_REPOSITORY.formatted(repositoryName));
308342
}

webapp/src/test/java/com/box/l10n/mojito/service/thirdparty/ThirdPartyTMSPhraseTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
package com.box.l10n.mojito.service.thirdparty;
22

3+
import static com.box.l10n.mojito.service.thirdparty.ThirdPartyTMSPhrase.areTagsWithin5Minutes;
4+
import static com.box.l10n.mojito.service.thirdparty.ThirdPartyTMSPhrase.uploadTagToLocalDateTime;
5+
import static org.junit.Assert.assertEquals;
6+
import static org.junit.Assert.assertFalse;
7+
import static org.junit.Assert.assertThrows;
8+
import static org.junit.Assert.assertTrue;
9+
310
import com.box.l10n.mojito.entity.Repository;
411
import com.box.l10n.mojito.json.ObjectMapper;
512
import com.box.l10n.mojito.service.assetExtraction.ServiceTestBase;
@@ -11,6 +18,8 @@
1118
import com.box.l10n.mojito.service.tm.search.TextUnitSearcherParameters;
1219
import com.box.l10n.mojito.test.TestIdWatcher;
1320
import com.google.common.collect.ImmutableMap;
21+
import java.time.LocalDateTime;
22+
import java.time.format.DateTimeFormatter;
1423
import java.util.List;
1524
import org.junit.Assume;
1625
import org.junit.Rule;
@@ -59,6 +68,14 @@ public void testBasics() throws RepositoryLocaleCreationException {
5968
null,
6069
null);
6170

71+
thirdPartyTMSPhrase.push(
72+
repository,
73+
testProjectId,
74+
thirdPartyServiceTestData.getPluralSeparator(),
75+
null,
76+
null,
77+
null);
78+
6279
thirdPartyTMSPhrase.pull(
6380
repository,
6481
testProjectId,
@@ -88,4 +105,36 @@ public void testBasics() throws RepositoryLocaleCreationException {
88105
// t));
89106

90107
}
108+
109+
@Test
110+
public void testUploadTagToLocalDateTimeValid() {
111+
LocalDateTime localDateTime = uploadTagToLocalDateTime("push_test_2024_11_21_18_55_38_004_502");
112+
assertEquals(
113+
"2024-11-21T18:55:38.004",
114+
localDateTime.format(DateTimeFormatter.ISO_DATE_TIME),
115+
"Parsed LocalDateTime does not match the expected value");
116+
}
117+
118+
@Test
119+
public void testUploadTagToLocalDateTimeInvalid() {
120+
IllegalArgumentException exception =
121+
assertThrows(
122+
IllegalArgumentException.class,
123+
() -> uploadTagToLocalDateTime("invalid_tag_2024_11_21_18_55_004"));
124+
assertTrue(exception.getMessage().contains("Invalid tag format"));
125+
}
126+
127+
@Test
128+
public void testTagsWithin5Minutes() {
129+
String tag1 = "push_test_2024_11_21_18_55_38_004_502";
130+
String tag2 = "push_test_2024_11_21_18_53_30_123_456";
131+
assertTrue(areTagsWithin5Minutes(tag1, tag2));
132+
}
133+
134+
@Test
135+
public void testTagsOutside5Minutes() {
136+
String tag1 = "push_test_2024_11_21_18_55_38_004_502";
137+
String tag2 = "push_test_2024_11_21_18_49_30_123_456";
138+
assertFalse(areTagsWithin5Minutes(tag1, tag2));
139+
}
91140
}

0 commit comments

Comments
 (0)