Skip to content
This repository was archived by the owner on Feb 24, 2026. It is now read-only.

Commit 34193b8

Browse files
authored
feat: add JsonWriterCache.java and added JsonWriterCache in DirectWriter to allow JsonWrites (#489)
* feat: add JsonDirectWriter * Added JsonWriterCache.java, added JsonWriterCache in DirectWriter, added test cases and changed some test file errors. * Add IT test * Fixed error in IT test * Fixed spelling in DirectWriter, fixed non static import * to be single class imports
1 parent b875340 commit 34193b8

File tree

7 files changed

+730
-9
lines changed

7 files changed

+730
-9
lines changed

google-cloud-bigquerystorage/src/main/java/com/google/cloud/bigquery/storage/v1alpha2/DirectWriter.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@
3333
import java.util.concurrent.locks.Lock;
3434
import java.util.concurrent.locks.ReentrantLock;
3535
import java.util.logging.Logger;
36+
import org.json.JSONArray;
3637

3738
/**
3839
* Writer that can help user to write data to BigQuery. This is a simplified version of the Write
3940
* API. For users writing with COMMITTED stream and don't care about row deduplication, it is
40-
* recommended to use this Writer.
41+
* recommended to use this Writer. The DirectWriter can be used to write both JSON and protobuf
42+
* data.
4143
*
4244
* <pre>{@code
4345
* DataProto data;
@@ -50,7 +52,9 @@
5052
public class DirectWriter {
5153
private static final Logger LOG = Logger.getLogger(DirectWriter.class.getName());
5254
private static WriterCache cache = null;
55+
private static JsonWriterCache jsonCache = null;
5356
private static Lock cacheLock = new ReentrantLock();
57+
private static Lock jsonCacheLock = new ReentrantLock();
5458

5559
/**
5660
* Append rows to the given table.
@@ -103,10 +107,53 @@ public Long apply(Storage.AppendRowsResponse appendRowsResponse) {
103107
MoreExecutors.directExecutor());
104108
}
105109

110+
/**
111+
* Append rows to the given table.
112+
*
113+
* @param tableName table name in the form of "projects/{pName}/datasets/{dName}/tables/{tName}"
114+
* @param json A JSONArray
115+
* @return A future that contains the offset at which the append happened. Only when the future
116+
* returns with valid offset, then the append actually happened.
117+
* @throws IOException, InterruptedException, InvalidArgumentException,
118+
* Descriptors.DescriptorValidationException
119+
*/
120+
public static ApiFuture<Long> append(String tableName, JSONArray json)
121+
throws IOException, InterruptedException, InvalidArgumentException,
122+
Descriptors.DescriptorValidationException {
123+
Preconditions.checkNotNull(tableName, "TableName is null.");
124+
Preconditions.checkNotNull(json, "JSONArray is null.");
125+
126+
if (json.length() == 0) {
127+
throw new InvalidArgumentException(
128+
new Exception("Empty JSONArrays are not allowed"),
129+
GrpcStatusCode.of(Status.Code.INVALID_ARGUMENT),
130+
false);
131+
}
132+
try {
133+
jsonCacheLock.lock();
134+
if (jsonCache == null) {
135+
jsonCache = JsonWriterCache.getInstance();
136+
}
137+
} finally {
138+
jsonCacheLock.unlock();
139+
}
140+
JsonStreamWriter writer = jsonCache.getTableWriter(tableName);
141+
return ApiFutures.<Storage.AppendRowsResponse, Long>transform(
142+
writer.append(json, /* offset = */ -1, /*allowUnknownFields = */ false),
143+
new ApiFunction<Storage.AppendRowsResponse, Long>() {
144+
@Override
145+
public Long apply(Storage.AppendRowsResponse appendRowsResponse) {
146+
return Long.valueOf(appendRowsResponse.getOffset());
147+
}
148+
},
149+
MoreExecutors.directExecutor());
150+
}
151+
106152
@VisibleForTesting
107153
public static void testSetStub(
108154
BigQueryWriteClient stub, int maxTableEntry, SchemaCompatibility schemaCheck) {
109155
cache = WriterCache.getTestInstance(stub, maxTableEntry, schemaCheck);
156+
jsonCache = JsonWriterCache.getTestInstance(stub, maxTableEntry);
110157
}
111158

112159
/** Clears the underlying cache and all the transport connections. */

google-cloud-bigquerystorage/src/main/java/com/google/cloud/bigquery/storage/v1alpha2/JsonStreamWriter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ public void close() {
260260
this.streamWriter.close();
261261
}
262262

263+
/** Returns if a stream has expired. */
264+
public Boolean expired() {
265+
return this.streamWriter.expired();
266+
}
267+
263268
private class JsonStreamWriterOnSchemaUpdateRunnable extends OnSchemaUpdateRunnable {
264269
private JsonStreamWriter jsonStreamWriter;
265270
/**
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigquery.storage.v1alpha2;
17+
18+
import com.google.common.annotations.VisibleForTesting;
19+
import com.google.common.base.Preconditions;
20+
import com.google.common.cache.Cache;
21+
import com.google.common.cache.CacheBuilder;
22+
import com.google.protobuf.Descriptors;
23+
import java.io.IOException;
24+
import java.util.concurrent.ConcurrentMap;
25+
import java.util.logging.Logger;
26+
import java.util.regex.Matcher;
27+
import java.util.regex.Pattern;
28+
29+
/**
30+
* A cache of JsonStreamWriters that can be looked up by Table Name. The entries will expire after 5
31+
* minutes if not used. Code sample: JsonWriterCache cache = JsonWriterCache.getInstance();
32+
* JsonStreamWriter writer = cache.getWriter(); // Use... cache.returnWriter(writer);
33+
*/
34+
public class JsonWriterCache {
35+
private static final Logger LOG = Logger.getLogger(JsonWriterCache.class.getName());
36+
37+
private static String tablePatternString = "(projects/[^/]+/datasets/[^/]+/tables/[^/]+)";
38+
private static Pattern tablePattern = Pattern.compile(tablePatternString);
39+
40+
private static JsonWriterCache instance;
41+
private Cache<String, JsonStreamWriter> jsonWriterCache;
42+
43+
// Maximum number of tables to hold in the cache, once the maxium exceeded, the cache will be
44+
// evicted based on least recent used.
45+
private static final int MAX_TABLE_ENTRY = 100;
46+
private static final int MAX_WRITERS_PER_TABLE = 1;
47+
48+
private final BigQueryWriteClient stub;
49+
50+
private JsonWriterCache(BigQueryWriteClient stub, int maxTableEntry) {
51+
this.stub = stub;
52+
jsonWriterCache =
53+
CacheBuilder.newBuilder().maximumSize(maxTableEntry).<String, JsonStreamWriter>build();
54+
}
55+
56+
public static JsonWriterCache getInstance() throws IOException {
57+
if (instance == null) {
58+
BigQueryWriteSettings stubSettings = BigQueryWriteSettings.newBuilder().build();
59+
BigQueryWriteClient stub = BigQueryWriteClient.create(stubSettings);
60+
instance = new JsonWriterCache(stub, MAX_TABLE_ENTRY);
61+
}
62+
return instance;
63+
}
64+
65+
/** Returns a cache with custom stub used by test. */
66+
@VisibleForTesting
67+
public static JsonWriterCache getTestInstance(BigQueryWriteClient stub, int maxTableEntry) {
68+
Preconditions.checkNotNull(stub, "Stub is null.");
69+
return new JsonWriterCache(stub, maxTableEntry);
70+
}
71+
72+
private Stream.WriteStream CreateNewWriteStream(String tableName) {
73+
Stream.WriteStream stream =
74+
Stream.WriteStream.newBuilder().setType(Stream.WriteStream.Type.COMMITTED).build();
75+
stream =
76+
stub.createWriteStream(
77+
Storage.CreateWriteStreamRequest.newBuilder()
78+
.setParent(tableName)
79+
.setWriteStream(stream)
80+
.build());
81+
LOG.info("Created write stream:" + stream.getName());
82+
return stream;
83+
}
84+
85+
JsonStreamWriter CreateNewWriter(Stream.WriteStream writeStream)
86+
throws IllegalArgumentException, IOException, InterruptedException,
87+
Descriptors.DescriptorValidationException {
88+
return JsonStreamWriter.newBuilder(writeStream.getName(), writeStream.getTableSchema())
89+
.setChannelProvider(stub.getSettings().getTransportChannelProvider())
90+
.setCredentialsProvider(stub.getSettings().getCredentialsProvider())
91+
.setExecutorProvider(stub.getSettings().getExecutorProvider())
92+
.build();
93+
}
94+
/**
95+
* Gets a writer for a given table with the given tableName
96+
*
97+
* @param tableName
98+
* @return
99+
* @throws Exception
100+
*/
101+
public JsonStreamWriter getTableWriter(String tableName)
102+
throws IllegalArgumentException, IOException, InterruptedException,
103+
Descriptors.DescriptorValidationException {
104+
Preconditions.checkNotNull(tableName, "TableName is null.");
105+
Matcher matcher = tablePattern.matcher(tableName);
106+
if (!matcher.matches()) {
107+
throw new IllegalArgumentException("Invalid table name: " + tableName);
108+
}
109+
110+
Stream.WriteStream writeStream = null;
111+
JsonStreamWriter writer = null;
112+
113+
synchronized (this) {
114+
writer = jsonWriterCache.getIfPresent(tableName);
115+
if (writer != null) {
116+
if (!writer.expired()) {
117+
return writer;
118+
} else {
119+
writer.close();
120+
}
121+
}
122+
writeStream = CreateNewWriteStream(tableName);
123+
writer = CreateNewWriter(writeStream);
124+
jsonWriterCache.put(tableName, writer);
125+
}
126+
return writer;
127+
}
128+
129+
/** Clear the cache and close all the writers in the cache. */
130+
public void clear() {
131+
synchronized (this) {
132+
ConcurrentMap<String, JsonStreamWriter> map = jsonWriterCache.asMap();
133+
for (String key : map.keySet()) {
134+
JsonStreamWriter entry = jsonWriterCache.getIfPresent(key);
135+
entry.close();
136+
}
137+
jsonWriterCache.cleanUp();
138+
}
139+
}
140+
141+
@VisibleForTesting
142+
public long cachedTableCount() {
143+
synchronized (jsonWriterCache) {
144+
return jsonWriterCache.size();
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)