Skip to content

Commit edde56a

Browse files
feat(bigtable): expose the row key, mutation count and serialized size on RowMutationEntry
RowMutationEntry exposed no accessor for its serialized size, its row key or its mutation count, so a caller that needs any of them had to call the @internalapi toProto() and build a whole MutateRowsRequest.Entry to read one number. Bounding in-flight memory by bytes is the motivating case: a single row mutation may be megabytes, so bounding by entry count bounds nothing useful. Mutation.byteSize could not simply be exposed - it sums each child mutation's own serialized size and omits the row-key field and the per-mutation tag and length prefixes. Mutation now accumulates the framed size alongside it, and RowMutationEntry.getSerializedSize() adds the row-key field to it. Reads are O(1) and allocate nothing. The counters are maintained on the fromProto factories too, which bypass addMutation, and are transient and recomputed in readObject so an instance restored from an older serialized form is correct rather than reporting zero. numMutations and byteSize are deliberately left alone.
1 parent 9337a93 commit edde56a

3 files changed

Lines changed: 197 additions & 0 deletions

File tree

‎java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Mutation.java‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.google.api.core.BetaApi;
1919
import com.google.api.core.InternalApi;
20+
import com.google.bigtable.v2.MutateRowsRequest;
2021
import com.google.bigtable.v2.Mutation.AddToCell;
2122
import com.google.bigtable.v2.Mutation.DeleteFromColumn;
2223
import com.google.bigtable.v2.Mutation.DeleteFromFamily;
@@ -28,6 +29,7 @@
2829
import com.google.common.collect.ImmutableList;
2930
import com.google.common.primitives.Longs;
3031
import com.google.protobuf.ByteString;
32+
import com.google.protobuf.CodedOutputStream;
3133
import java.io.IOException;
3234
import java.io.ObjectInputStream;
3335
import java.io.ObjectOutputStream;
@@ -60,6 +62,23 @@ public final class Mutation implements MutationApi<Mutation>, Serializable {
6062
private int numMutations;
6163
private long byteSize;
6264

65+
/**
66+
* Number of accumulated mutations, maintained on every path that adds one — unlike {@link
67+
* #numMutations}, which counts only what went through {@link #addMutation} and stays at zero for
68+
* the {@code fromProto} factories.
69+
*/
70+
private transient int mutationCount;
71+
72+
/**
73+
* Size these mutations occupy inside a request proto that carries them as a repeated field: each
74+
* mutation's tag, length prefix and body. Backs {@link RowMutationEntry#getSerializedSize()} so
75+
* callers can size an entry without building one.
76+
*
77+
* <p>Transient and recomputed by {@code readObject}, so an instance restored from a form written
78+
* by an older version is correct too.
79+
*/
80+
private transient long mutationsSerializedSize;
81+
6382
/** Creates new instance of Mutation object. */
6483
public static Mutation create() {
6584
return new Mutation(false);
@@ -86,6 +105,7 @@ public static Mutation createUnsafe() {
86105
public static Mutation fromProtoUnsafe(List<com.google.bigtable.v2.Mutation> protos) {
87106
Mutation mutation = new Mutation(true);
88107
mutation.mutations.addAll(protos);
108+
mutation.countAllTowardsSerializedSize(protos);
89109
return mutation;
90110
}
91111

@@ -99,6 +119,7 @@ public static Mutation fromProtoUnsafe(List<com.google.bigtable.v2.Mutation> pro
99119
public static Mutation fromProtoUnsafe(Iterable<com.google.bigtable.v2.Mutation> protos) {
100120
Mutation mutation = new Mutation(true);
101121
mutation.mutations.addAll(protos);
122+
mutation.countAllTowardsSerializedSize(protos);
102123
return mutation;
103124
}
104125

@@ -115,6 +136,7 @@ public static Mutation fromProtoUnsafe(Iterable<com.google.bigtable.v2.Mutation>
115136
static Mutation fromProto(List<com.google.bigtable.v2.Mutation> protos) {
116137
Mutation mutation = new Mutation(false);
117138
mutation.mutations.addAll(protos);
139+
mutation.countAllTowardsSerializedSize(protos);
118140
return mutation;
119141
}
120142

@@ -129,6 +151,7 @@ private void readObject(ObjectInputStream input) throws IOException, ClassNotFou
129151
ImmutableList<com.google.bigtable.v2.Mutation> deserialized =
130152
(ImmutableList<com.google.bigtable.v2.Mutation>) input.readObject();
131153
this.mutations = ImmutableList.<com.google.bigtable.v2.Mutation>builder().addAll(deserialized);
154+
countAllTowardsSerializedSize(deserialized);
132155
}
133156

134157
private void writeObject(ObjectOutputStream output) throws IOException {
@@ -335,10 +358,38 @@ private void addMutation(com.google.bigtable.v2.Mutation mutation) {
335358

336359
numMutations++;
337360
byteSize += mutation.getSerializedSize();
361+
countTowardsSerializedSize(mutation);
338362

339363
mutations.add(mutation);
340364
}
341365

366+
/** Accumulates one mutation into {@link #mutationCount} and {@link #mutationsSerializedSize}. */
367+
private void countTowardsSerializedSize(com.google.bigtable.v2.Mutation mutation) {
368+
mutationCount++;
369+
// The field number only decides the tag's own width, and every request proto that carries
370+
// these mutations numbers the field below 16, so this is the same one byte in all of them.
371+
mutationsSerializedSize +=
372+
CodedOutputStream.computeMessageSize(
373+
MutateRowsRequest.Entry.MUTATIONS_FIELD_NUMBER, mutation);
374+
}
375+
376+
/** Accumulates mutations added without going through {@link #addMutation}. */
377+
private void countAllTowardsSerializedSize(Iterable<com.google.bigtable.v2.Mutation> protos) {
378+
for (com.google.bigtable.v2.Mutation proto : protos) {
379+
countTowardsSerializedSize(proto);
380+
}
381+
}
382+
383+
/** Returns the number of accumulated mutations, on every construction path. */
384+
int getMutationCount() {
385+
return mutationCount;
386+
}
387+
388+
/** Returns {@link #mutationsSerializedSize}. */
389+
long getMutationsSerializedSize() {
390+
return mutationsSerializedSize;
391+
}
392+
342393
private static ByteString wrapByteString(String str) {
343394
if (str == null) {
344395
return null;

‎java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/RowMutationEntry.java‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.bigtable.v2.MutateRowsRequest;
2121
import com.google.common.base.Preconditions;
2222
import com.google.protobuf.ByteString;
23+
import com.google.protobuf.CodedOutputStream;
2324
import java.io.Serializable;
2425
import javax.annotation.Nonnull;
2526

@@ -200,6 +201,35 @@ public RowMutationEntry mergeToCell(
200201
return this;
201202
}
202203

204+
/** Returns the row key these mutations apply to. */
205+
public ByteString getRowKey() {
206+
return key;
207+
}
208+
209+
/** Returns the number of mutations accumulated in this entry. */
210+
public int getMutationCount() {
211+
return mutation.getMutationCount();
212+
}
213+
214+
/**
215+
* Returns the serialized size, in bytes, of the request entry this becomes on the wire — the same
216+
* number as {@code toProto().getSerializedSize()}, without building the proto.
217+
*
218+
* <p>Intended for callers that bound their own in-flight memory or report a bytes-written metric.
219+
* A single row mutation may legitimately be megabytes, so bounding by entry count bounds nothing
220+
* useful; this is the number that bounds memory. It is accumulated as mutations are added rather
221+
* than computed on each call.
222+
*/
223+
public long getSerializedSize() {
224+
long size = mutation.getMutationsSerializedSize();
225+
if (!key.isEmpty()) {
226+
// Implicit presence: an empty row key is not written at all, so it contributes no tag and
227+
// no length prefix either.
228+
size += CodedOutputStream.computeBytesSize(MutateRowsRequest.Entry.ROW_KEY_FIELD_NUMBER, key);
229+
}
230+
return size;
231+
}
232+
203233
@InternalApi
204234
public MutateRowsRequest.Entry toProto() {
205235
Preconditions.checkArgument(

‎java-bigtable/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/RowMutationEntryTest.java‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,120 @@ public void testWithLongValue() {
165165
.setValue(ByteString.copyFrom(Longs.toByteArray(100_000L)))
166166
.build());
167167
}
168+
169+
@Test
170+
public void getRowKeyTest() {
171+
ByteString rowKey = ByteString.copyFromUtf8("row-key");
172+
173+
assertThat(RowMutationEntry.create(rowKey).getRowKey()).isEqualTo(rowKey);
174+
assertThat(RowMutationEntry.create("row-key").getRowKey()).isEqualTo(rowKey);
175+
assertThat(RowMutationEntry.create(ByteString.EMPTY).getRowKey()).isEqualTo(ByteString.EMPTY);
176+
}
177+
178+
@Test
179+
public void getMutationCountTest() {
180+
assertThat(RowMutationEntry.create("row-key").getMutationCount()).isEqualTo(0);
181+
assertThat(
182+
RowMutationEntry.create("row-key")
183+
.setCell("fake-family", "q1", 10_000L, "v1")
184+
.setCell("fake-family", "q2", 10_000L, "v2")
185+
.deleteFamily("fake-family")
186+
.deleteRow()
187+
.getMutationCount())
188+
.isEqualTo(4);
189+
}
190+
191+
@Test
192+
public void getSerializedSizeMatchesProtoTest() {
193+
// Every shape that changes the framing: no mutations, an empty row key, a value long enough
194+
// to need a two-byte length prefix, and each mutation kind.
195+
assertSizeMatchesProto(RowMutationEntry.create("row-key"));
196+
assertSizeMatchesProto(RowMutationEntry.create(ByteString.EMPTY));
197+
assertSizeMatchesProto(
198+
RowMutationEntry.create("row-key").setCell("fake-family", "q", 10_000L, "v"));
199+
assertSizeMatchesProto(
200+
RowMutationEntry.create("row-key").setCell("fake-family", "q", 10_000L, repeat("v", 200)));
201+
assertSizeMatchesProto(
202+
RowMutationEntry.create(ByteString.copyFromUtf8(repeat("k", 5_000)))
203+
.setCell("fake-family", "q", 10_000L, repeat("v", 70_000)));
204+
assertSizeMatchesProto(
205+
RowMutationEntry.create("row-key")
206+
.setCell("fake-family", "q1", 10_000L, "v1")
207+
.setCell("fake-family", "q2", 10_000L, 100_000L)
208+
.deleteCells("fake-family", "q1")
209+
.deleteFamily("fake-family")
210+
.deleteRow());
211+
}
212+
213+
@Test
214+
public void getSerializedSizeMatchesProtoForManyMutationsTest() {
215+
// Crosses the point where the repeated field's own count changes nothing but the accumulated
216+
// per-mutation length prefixes do.
217+
RowMutationEntry underTest = RowMutationEntry.create("row-key");
218+
for (int i = 0; i < 300; i++) {
219+
underTest.setCell("fake-family", "q" + i, 10_000L, "value-" + i);
220+
}
221+
222+
assertThat(underTest.getMutationCount()).isEqualTo(300);
223+
assertSizeMatchesProto(underTest);
224+
}
225+
226+
@Test
227+
public void getSerializedSizeFromMutationUnsafeTest() {
228+
// createFromMutationUnsafe bypasses addMutation, so the accumulated size has to be maintained
229+
// by the fromProto factories too.
230+
com.google.cloud.bigtable.data.v2.models.Mutation mutation =
231+
com.google.cloud.bigtable.data.v2.models.Mutation.fromProtoUnsafe(
232+
ImmutableList.of(
233+
Mutation.newBuilder()
234+
.setSetCell(
235+
Mutation.SetCell.newBuilder()
236+
.setFamilyName("fake-family")
237+
.setColumnQualifier(ByteString.copyFromUtf8("q"))
238+
.setTimestampMicros(10_000L)
239+
.setValue(ByteString.copyFromUtf8(repeat("v", 500))))
240+
.build(),
241+
Mutation.newBuilder()
242+
.setDeleteFromRow(Mutation.DeleteFromRow.getDefaultInstance())
243+
.build()));
244+
RowMutationEntry underTest =
245+
RowMutationEntry.createFromMutationUnsafe(ByteString.copyFromUtf8("row-key"), mutation);
246+
247+
assertThat(underTest.getMutationCount()).isEqualTo(2);
248+
assertSizeMatchesProto(underTest);
249+
}
250+
251+
@Test
252+
public void getSerializedSizeSurvivesSerializationTest()
253+
throws IOException, ClassNotFoundException {
254+
RowMutationEntry underTest =
255+
RowMutationEntry.create("row-key")
256+
.setCell("fake-family", "q", 10_000L, repeat("v", 500))
257+
.deleteFamily("fake-family");
258+
259+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
260+
ObjectOutputStream oos = new ObjectOutputStream(bos);
261+
oos.writeObject(underTest);
262+
oos.close();
263+
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
264+
RowMutationEntry actual = (RowMutationEntry) ois.readObject();
265+
266+
assertThat(actual.getRowKey()).isEqualTo(underTest.getRowKey());
267+
assertThat(actual.getMutationCount()).isEqualTo(underTest.getMutationCount());
268+
assertThat(actual.getSerializedSize()).isEqualTo(underTest.getSerializedSize());
269+
assertSizeMatchesProto(actual);
270+
}
271+
272+
private static void assertSizeMatchesProto(RowMutationEntry entry) {
273+
assertThat(entry.getSerializedSize()).isEqualTo(entry.toProto().getSerializedSize());
274+
assertThat(entry.getMutationCount()).isEqualTo(entry.toProto().getMutationsCount());
275+
}
276+
277+
private static String repeat(String unit, int times) {
278+
StringBuilder sb = new StringBuilder(unit.length() * times);
279+
for (int i = 0; i < times; i++) {
280+
sb.append(unit);
281+
}
282+
return sb.toString();
283+
}
168284
}

0 commit comments

Comments
 (0)