Skip to content

Commit 44e9dd5

Browse files
authored
fix: update RecoveryFileManager to allow distinct files for multiple invocations of equivalent info (#2207)
When creating a new recovery file it is important that distinct recovery files be created always, even if the provided BlobInfo is equivalent to a previously created one. Allocate a UUID, and hash it with goodFastHash(64) before encoding to base64 (url safe).
1 parent 087b2e4 commit 44e9dd5

File tree

2 files changed

+55
-3
lines changed

2 files changed

+55
-3
lines changed

google-cloud-storage/src/main/java/com/google/cloud/storage/RecoveryFileManager.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,29 @@
1919
import static com.google.common.base.Preconditions.checkArgument;
2020

2121
import com.google.common.collect.ImmutableList;
22-
import com.google.common.primitives.Ints;
22+
import com.google.common.hash.HashCode;
23+
import com.google.common.hash.HashFunction;
24+
import com.google.common.hash.Hasher;
25+
import com.google.common.hash.Hashing;
2326
import java.io.IOException;
27+
import java.nio.charset.StandardCharsets;
2428
import java.nio.file.Files;
2529
import java.nio.file.Path;
2630
import java.util.Base64;
2731
import java.util.Collections;
2832
import java.util.HashMap;
2933
import java.util.List;
3034
import java.util.Map;
35+
import java.util.UUID;
3136

3237
final class RecoveryFileManager {
3338

3439
private final ImmutableList<RecoveryVolume> volumes;
3540
/** Keep track of active info and file */
3641
private final Map<BlobInfo, RecoveryFile> files;
3742

43+
private final HashFunction hashFunction;
44+
3845
/**
3946
* Round-robin assign recovery files to the configured volumes. Use this index to keep track of
4047
* which volume to assign to next.
@@ -45,13 +52,18 @@ private RecoveryFileManager(List<RecoveryVolume> volumes) {
4552
this.volumes = ImmutableList.copyOf(volumes);
4653
this.files = Collections.synchronizedMap(new HashMap<>());
4754
this.nextVolumeIndex = 0;
55+
this.hashFunction = Hashing.goodFastHash(64);
4856
}
4957

58+
@SuppressWarnings("UnstableApiUsage")
5059
public RecoveryFile newRecoveryFile(BlobInfo info) {
5160
int i = getNextVolumeIndex();
5261
RecoveryVolume v = volumes.get(i);
53-
int hashCode = info.hashCode();
54-
String fileName = Base64.getUrlEncoder().encodeToString(Ints.toByteArray(hashCode));
62+
UUID uuid = UUID.randomUUID();
63+
String string = uuid.toString();
64+
Hasher hasher = hashFunction.newHasher();
65+
HashCode hash = hasher.putString(string, StandardCharsets.UTF_8).hash();
66+
String fileName = Base64.getUrlEncoder().encodeToString(hash.asBytes());
5567
Path path = v.basePath.resolve(fileName);
5668
RecoveryFile recoveryFile = new RecoveryFile(path, v.sink, () -> files.remove(info));
5769
files.put(info, recoveryFile);

google-cloud-storage/src/test/java/com/google/cloud/storage/RecoveryFileManagerTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
package com.google.cloud.storage;
1818

19+
import static com.google.cloud.storage.TestUtils.assertAll;
20+
import static com.google.cloud.storage.TestUtils.xxd;
1921
import static com.google.common.truth.Truth.assertThat;
22+
import static com.google.common.truth.Truth.assertWithMessage;
2023
import static org.junit.Assert.assertThrows;
2124

2225
import com.google.common.collect.ImmutableList;
@@ -33,6 +36,7 @@
3336
import java.time.Duration;
3437
import java.time.Instant;
3538
import java.util.Objects;
39+
import java.util.Random;
3640
import java.util.stream.Stream;
3741
import org.junit.Rule;
3842
import org.junit.Test;
@@ -143,4 +147,40 @@ public void fileAssignmentIsRoundRobin() throws IOException {
143147
assertThat(parentDirs).isEqualTo(ImmutableSet.of(tempDir1, tempDir2, tempDir3));
144148
}
145149
}
150+
151+
@Test
152+
public void multipleRecoveryFilesForEqualBlobInfoAreAbleToExistConcurrently() throws Exception {
153+
Path tempDir = temporaryFolder.newFolder(testName.getMethodName()).toPath();
154+
RecoveryFileManager rfm =
155+
RecoveryFileManager.of(
156+
ImmutableList.of(tempDir),
157+
path -> ThroughputSink.logged(path.toAbsolutePath().toString(), clock));
158+
159+
BlobInfo info = BlobInfo.newBuilder("bucket", "object").build();
160+
try (RecoveryFile rf1 = rfm.newRecoveryFile(info);
161+
RecoveryFile rf2 = rfm.newRecoveryFile(info); ) {
162+
163+
Random rand = new Random(467123);
164+
byte[] bytes1 = DataGenerator.rand(rand).genBytes(7);
165+
byte[] bytes2 = DataGenerator.rand(rand).genBytes(41);
166+
try (WritableByteChannel writer = rf1.writer()) {
167+
writer.write(ByteBuffer.wrap(bytes1));
168+
}
169+
try (WritableByteChannel writer = rf2.writer()) {
170+
writer.write(ByteBuffer.wrap(bytes2));
171+
}
172+
173+
byte[] actual1 = ByteStreams.toByteArray(Files.newInputStream(rf1.getPath()));
174+
byte[] actual2 = ByteStreams.toByteArray(Files.newInputStream(rf2.getPath()));
175+
176+
String expected1 = xxd(bytes1);
177+
String expected2 = xxd(bytes2);
178+
179+
String xxd1 = xxd(actual1);
180+
String xxd2 = xxd(actual2);
181+
assertAll(
182+
() -> assertWithMessage("rf1 should contain bytes1").that(xxd1).isEqualTo(expected1),
183+
() -> assertWithMessage("rf2 should contain bytes2").that(xxd2).isEqualTo(expected2));
184+
}
185+
}
146186
}

0 commit comments

Comments
 (0)