All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.bigquery.storage.v1.StreamWriter Maven / Gradle / Ivy

There is a newer version: 3.11.0
Show newest version
/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.cloud.bigquery.storage.v1;

import com.google.api.core.ApiFuture;
import com.google.api.gax.batching.FlowController;
import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.core.ExecutorProvider;
import com.google.api.gax.retrying.RetrySettings;
import com.google.api.gax.rpc.TransportChannelProvider;
import com.google.auto.value.AutoOneOf;
import com.google.auto.value.AutoValue;
import com.google.cloud.bigquery.storage.v1.AppendRowsRequest.MissingValueInterpretation;
import com.google.cloud.bigquery.storage.v1.ConnectionWorker.AppendRequestAndResponse;
import com.google.cloud.bigquery.storage.v1.ConnectionWorker.TableSchemaAndTimestamp;
import com.google.cloud.bigquery.storage.v1.StreamWriter.SingleConnectionOrConnectionPool.Kind;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import io.grpc.Status;
import io.grpc.Status.Code;
import io.grpc.StatusRuntimeException;
import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A BigQuery Stream Writer that can be used to write data into BigQuery Table.
 *
 * 

TODO: Support batching. */ public class StreamWriter implements AutoCloseable { private static final Logger log = Logger.getLogger(StreamWriter.class.getName()); private static String datasetsMatching = "projects/[^/]+/datasets/[^/]+/"; private static Pattern streamPatternDatasets = Pattern.compile(datasetsMatching); private static String defaultStreamMatching = "/_default"; private static Pattern streamPatternDefaultStream = Pattern.compile(defaultStreamMatching); // Cache of location info for a given dataset. private static Map projectAndDatasetToLocation = new ConcurrentHashMap<>(); // Map of fields to their MissingValueInterpretation, which dictates how a field should be // populated when it is missing from an input user row. private Map missingValueInterpretationMap = new HashMap(); /* * The identifier of stream to write to. */ private final String streamName; /** Every writer has a fixed proto schema. */ private final ProtoSchema writerSchema; /* * Location of the destination. */ private final String location; /* * If user has closed the StreamWriter. */ private AtomicBoolean userClosed = new AtomicBoolean(false); /* * A String that uniquely identifies this writer. */ private final String writerId = UUID.randomUUID().toString(); /** * The default missing value interpretation if the column has default value defined but not * presented in the missing value map. */ private AppendRowsRequest.MissingValueInterpretation defaultMissingValueInterpretation = MissingValueInterpretation.MISSING_VALUE_INTERPRETATION_UNSPECIFIED; /** * Stream can access a single connection or a pool of connection depending on whether multiplexing * is enabled. */ private final SingleConnectionOrConnectionPool singleConnectionOrConnectionPool; /** Test only param to tell how many times a client is created. */ private static int testOnlyClientCreatedTimes = 0; /** * Static map from {@link ConnectionPoolKey} to connection pool. Note this map is static to be * shared by every stream writer in the same process. */ private static final Map connectionPoolMap = new ConcurrentHashMap<>(); /** Creation timestamp of this streamwriter */ private final long creationTimestamp; private Lock lock; /** The maximum size of one request. Defined by the API. */ public static long getApiMaxRequestBytes() { return 10L * 1000L * 1000L; // 10 megabytes (https://en.wikipedia.org/wiki/Megabyte) } /** * Connection pool with different key will be split. * *

Shard based only on location right now. */ @AutoValue abstract static class ConnectionPoolKey { abstract String location(); public static ConnectionPoolKey create(String location) { return new AutoValue_StreamWriter_ConnectionPoolKey(location); } } /** * When in single table mode, append directly to connectionWorker. Otherwise append to connection * pool in multiplexing mode. */ @AutoOneOf(SingleConnectionOrConnectionPool.Kind.class) public abstract static class SingleConnectionOrConnectionPool { /** Kind of connection operation mode. */ public enum Kind { CONNECTION_WORKER, CONNECTION_WORKER_POOL } public abstract Kind getKind(); public abstract ConnectionWorker connectionWorker(); public abstract ConnectionWorkerPool connectionWorkerPool(); public ApiFuture append( StreamWriter streamWriter, ProtoRows protoRows, long offset) { if (getKind() == Kind.CONNECTION_WORKER) { return connectionWorker().append(streamWriter, protoRows, offset); } else { return connectionWorkerPool().append(streamWriter, protoRows, offset); } } public void close(StreamWriter streamWriter) { if (getKind() == Kind.CONNECTION_WORKER) { connectionWorker().close(); } else { connectionWorkerPool().close(streamWriter); } } long getInflightWaitSeconds(StreamWriter streamWriter) { if (getKind() == Kind.CONNECTION_WORKER_POOL) { return connectionWorkerPool().getInflightWaitSeconds(streamWriter); } return connectionWorker().getInflightWaitSeconds(); } TableSchemaAndTimestamp getUpdatedSchema(StreamWriter streamWriter) { if (getKind() == Kind.CONNECTION_WORKER_POOL) { return connectionWorkerPool().getUpdatedSchema(streamWriter); } // Always populate MIN timestamp to w return connectionWorker().getUpdatedSchema(); } String getWriterId(String streamWriterId) { if (getKind() == Kind.CONNECTION_WORKER_POOL) { return streamWriterId; } return connectionWorker().getWriterId(); } public static SingleConnectionOrConnectionPool ofSingleConnection(ConnectionWorker connection) { return AutoOneOf_StreamWriter_SingleConnectionOrConnectionPool.connectionWorker(connection); } public static SingleConnectionOrConnectionPool ofConnectionPool( ConnectionWorkerPool connectionPool) { return AutoOneOf_StreamWriter_SingleConnectionOrConnectionPool.connectionWorkerPool( connectionPool); } } private StreamWriter(Builder builder) throws IOException { this.streamName = builder.streamName; this.writerSchema = builder.writerSchema; this.defaultMissingValueInterpretation = builder.defaultMissingValueInterpretation; BigQueryWriteSettings clientSettings = getBigQueryWriteSettings(builder); if (!builder.enableConnectionPool) { this.location = builder.location; this.singleConnectionOrConnectionPool = SingleConnectionOrConnectionPool.ofSingleConnection( new ConnectionWorker( builder.streamName, builder.location, builder.writerSchema, builder.maxInflightRequest, builder.maxInflightBytes, builder.maxRetryDuration, builder.limitExceededBehavior, builder.traceId, builder.compressorName, clientSettings, builder.retrySettings)); } else { if (!isDefaultStream(streamName)) { log.warning( "Connection pool is only allowed in default stream! However received " + builder.streamName); throw new IllegalArgumentException( "Trying to enable connection pool in non-default stream."); } if (builder.retrySettings != null) { log.warning("Retry settings is only allowed when connection pool is not enabled."); throw new IllegalArgumentException( "Trying to enable connection pool while providing retry settings."); } // We need a client to perform some getWriteStream calls. BigQueryWriteClient client = builder.client != null ? builder.client : new BigQueryWriteClient(clientSettings); String location = builder.location; if (location == null || location.isEmpty()) { // Location is not passed in, try to fetch from RPC String datasetAndProjectName = extractDatasetAndProjectName(builder.streamName); location = projectAndDatasetToLocation.computeIfAbsent( datasetAndProjectName, (key) -> { GetWriteStreamRequest writeStreamRequest = GetWriteStreamRequest.newBuilder() .setName(this.getStreamName()) .setView(WriteStreamView.BASIC) .build(); WriteStream writeStream = client.getWriteStream(writeStreamRequest); TableSchema writeStreamTableSchema = writeStream.getTableSchema(); String fetchedLocation = writeStream.getLocation(); log.info( String.format( "Fethed location %s for stream name %s, extracted project and dataset" + " name: %s\"", fetchedLocation, streamName, datasetAndProjectName)); return fetchedLocation; }); if (location.isEmpty()) { throw new IllegalStateException( String.format( "The location is empty for both user passed in value and looked up value for " + "stream: %s, extracted project and dataset name: %s", streamName, datasetAndProjectName)); } } this.location = location; // Assume the connection in the same pool share the same client and trace id. // The first StreamWriter for a new stub will create the pool for the other // streams in the same region, meaning the per StreamWriter settings are no // longer working unless all streams share the same set of settings this.singleConnectionOrConnectionPool = SingleConnectionOrConnectionPool.ofConnectionPool( connectionPoolMap.computeIfAbsent( ConnectionPoolKey.create(location), (key) -> { return new ConnectionWorkerPool( builder.maxInflightRequest, builder.maxInflightBytes, builder.maxRetryDuration, builder.limitExceededBehavior, builder.traceId, builder.compressorName, client.getSettings()); })); validateFetchedConnectonPool(builder); // If the client is not from outside, then shutdown the client we created. if (builder.client == null) { client.shutdown(); try { client.awaitTermination(150, TimeUnit.SECONDS); } catch (InterruptedException unused) { // Ignore interruption as this client is not used. } client.close(); } } this.creationTimestamp = System.nanoTime(); } @VisibleForTesting static String extractDatasetAndProjectName(String streamName) { Matcher streamMatcher = streamPatternDatasets.matcher(streamName); if (streamMatcher.find()) { return streamMatcher.group(); } else { throw new IllegalStateException( String.format("The passed in stream name does not match standard format %s", streamName)); } } @VisibleForTesting static boolean isDefaultStream(String streamName) { Matcher streamMatcher = streamPatternDefaultStream.matcher(streamName); return streamMatcher.find(); } AppendRowsRequest.MissingValueInterpretation getDefaultValueInterpretation() { return defaultMissingValueInterpretation; } static BigQueryWriteSettings getBigQueryWriteSettings(Builder builder) throws IOException { BigQueryWriteSettings.Builder settingsBuilder = null; if (builder.client != null) { settingsBuilder = builder.client.getSettings().toBuilder(); } else { settingsBuilder = new BigQueryWriteSettings.Builder() .setTransportChannelProvider( BigQueryWriteSettings.defaultGrpcTransportProviderBuilder() .setKeepAliveTime(org.threeten.bp.Duration.ofMinutes(1)) .setKeepAliveTimeout(org.threeten.bp.Duration.ofMinutes(1)) .setKeepAliveWithoutCalls(true) .setChannelsPerCpu(2) .build()) .setCredentialsProvider( BigQueryWriteSettings.defaultCredentialsProviderBuilder().build()) .setBackgroundExecutorProvider( BigQueryWriteSettings.defaultExecutorProviderBuilder().build()) .setEndpoint(BigQueryWriteSettings.getDefaultEndpoint()); } if (builder.channelProvider != null) { settingsBuilder.setTransportChannelProvider(builder.channelProvider); } if (builder.credentialsProvider != null) { settingsBuilder.setCredentialsProvider(builder.credentialsProvider); } if (builder.executorProvider != null) { settingsBuilder.setBackgroundExecutorProvider(builder.executorProvider); } if (builder.endpoint != null) { settingsBuilder.setEndpoint(builder.endpoint); } return settingsBuilder.build(); } // Validate whether the fetched connection pool matched certain properties. private void validateFetchedConnectonPool(StreamWriter.Builder builder) { String storedTraceId = this.singleConnectionOrConnectionPool.connectionWorkerPool().getTraceId(); if (!Objects.equals(storedTraceId, builder.traceId)) { throw new IllegalArgumentException( String.format( "Trace id used for the same connection pool for the same location must be the same, " + "however stored trace id is %s, and expected trace id is %s.", storedTraceId, builder.traceId)); } FlowController.LimitExceededBehavior storedLimitExceededBehavior = singleConnectionOrConnectionPool.connectionWorkerPool().limitExceededBehavior(); if (!Objects.equals(storedLimitExceededBehavior, builder.limitExceededBehavior)) { throw new IllegalArgumentException( String.format( "Limit exceeded behavior setting used for the same connection pool for the same " + "location must be the same, however stored value is %s, and expected " + "value is %s.", storedLimitExceededBehavior, builder.limitExceededBehavior)); } } /** * Sets the missing value interpretation map for the stream writer. The input * missingValueInterpretationMap is used for all write requests unless otherwise changed. * * @param missingValueInterpretationMap the missing value interpretation map used by stream * writer. */ public void setMissingValueInterpretationMap( Map missingValueInterpretationMap) { this.missingValueInterpretationMap = missingValueInterpretationMap; } /** * Schedules the writing of rows at the end of current stream. * * @param rows the rows in serialized format to write to BigQuery. * @return the append response wrapped in a future. */ public ApiFuture append(ProtoRows rows) { return append(rows, -1); } /** * Schedules the writing of rows at given offset. * *

Example of writing rows with specific offset. * *

{@code
   * ApiFuture future = writer.append(rows, 0);
   * ApiFutures.addCallback(future, new ApiFutureCallback() {
   *   public void onSuccess(AppendRowsResponse response) {
   *     if (!response.hasError()) {
   *       System.out.println("written with offset: " + response.getAppendResult().getOffset());
   *     } else {
   *       System.out.println("received an in stream error: " + response.getError().toString());
   *     }
   *   }
   *
   *   public void onFailure(Throwable t) {
   *     System.out.println("failed to write: " + t);
   *   }
   * }, MoreExecutors.directExecutor());
   * }
* * @param rows the rows in serialized format to write to BigQuery. * @param offset the offset of the first row. Provide -1 to write at the current end of stream. * @return the append response wrapped in a future. */ public ApiFuture append(ProtoRows rows, long offset) { if (userClosed.get()) { AppendRequestAndResponse requestWrapper = new AppendRequestAndResponse(AppendRowsRequest.newBuilder().build(), this, null); requestWrapper.appendResult.setException( new Exceptions.StreamWriterClosedException( Status.fromCode(Status.Code.FAILED_PRECONDITION) .withDescription("User closed StreamWriter"), streamName, getWriterId())); return requestWrapper.appendResult; } return this.singleConnectionOrConnectionPool.append(this, rows, offset); } /** * Returns the wait of a request in Client side before sending to the Server. Request could wait * in Client because it reached the client side inflight request limit (adjustable when * constructing the StreamWriter). The value is the wait time for the last sent request. A * constant high wait value indicates a need for more throughput, you can create a new Stream for * to increase the throughput in exclusive stream case, or create a new Writer in the default * stream case. */ public long getInflightWaitSeconds() { return singleConnectionOrConnectionPool.getInflightWaitSeconds(this); } /** @return a unique Id for the writer. */ public String getWriterId() { return singleConnectionOrConnectionPool.getWriterId(writerId); } /** @return name of the Stream that this writer is working on. */ public String getStreamName() { return streamName; } /** @return the passed in user schema. */ public ProtoSchema getProtoSchema() { return writerSchema; } /** @return the location of the destination. */ public String getLocation() { return location; } /** @return the missing value interpretation map used for the writer. */ public Map getMissingValueInterpretationMap() { return missingValueInterpretationMap; } /** * @return if a stream writer can no longer be used for writing. It is due to either the * StreamWriter is explicitly closed or the underlying connection is broken when connection * pool is not used. Client should recreate StreamWriter in this case. */ public boolean isClosed() { if (singleConnectionOrConnectionPool.getKind() == Kind.CONNECTION_WORKER) { return userClosed.get() || singleConnectionOrConnectionPool.connectionWorker().isConnectionInUnrecoverableState(); } else { // With ConnectionPool, we will replace the bad connection automatically. return userClosed.get(); } } /** @return if user explicitly closed the writer. */ public boolean isUserClosed() { return userClosed.get(); } /** Close the stream writer. Shut down all resources. */ @Override public void close() { userClosed.set(true); singleConnectionOrConnectionPool.close(this); } /** Constructs a new {@link StreamWriter.Builder} using the given stream and client. */ public static StreamWriter.Builder newBuilder(String streamName, BigQueryWriteClient client) { return new StreamWriter.Builder(streamName, client); } /** Constructs a new {@link StreamWriter.Builder} using the given stream. */ public static StreamWriter.Builder newBuilder(String streamName) { return new StreamWriter.Builder(streamName); } /** * Thread-safe getter of updated TableSchema. * *

This will return the updated schema only when the creation timestamp of this writer is older * than the updated schema. */ public synchronized TableSchema getUpdatedSchema() { TableSchemaAndTimestamp tableSchemaAndTimestamp = singleConnectionOrConnectionPool.getUpdatedSchema(this); if (tableSchemaAndTimestamp == null) { return null; } return creationTimestamp < tableSchemaAndTimestamp.updateTimeStamp() ? tableSchemaAndTimestamp.updatedSchema() : null; } /** * Sets the maximum time a request is allowed to be waiting in request waiting queue. Under very * low chance, it's possible for append request to be waiting indefintely for request callback * when Google networking SDK does not detect the networking breakage. The default timeout is 15 * minutes. We are investigating the root cause for callback not triggered by networking SDK. */ public static void setMaxRequestCallbackWaitTime(Duration waitTime) { ConnectionWorker.MAXIMUM_REQUEST_CALLBACK_WAIT_TIME = waitTime; } long getCreationTimestamp() { return creationTimestamp; } @VisibleForTesting SingleConnectionOrConnectionPool.Kind getConnectionOperationType() { return singleConnectionOrConnectionPool.getKind(); } @VisibleForTesting static int getTestOnlyClientCreatedTimes() { return testOnlyClientCreatedTimes; } @VisibleForTesting static void cleanUp() { testOnlyClientCreatedTimes = 0; connectionPoolMap.clear(); } @VisibleForTesting ConnectionWorkerPool getTestOnlyConnectionWorkerPool() { ConnectionWorkerPool connectionWorkerPool = null; for (Entry entry : connectionPoolMap.entrySet()) { connectionWorkerPool = entry.getValue(); } return connectionWorkerPool; } // A method to clear the static connectio pool to avoid making pool visible to other tests. @VisibleForTesting static void clearConnectionPool() { connectionPoolMap.clear(); } /** A builder of {@link StreamWriter}s. */ public static final class Builder { private static final long DEFAULT_MAX_INFLIGHT_REQUESTS = 1000L; private static final long DEFAULT_MAX_INFLIGHT_BYTES = 100 * 1024 * 1024; // 100Mb. private String streamName; private BigQueryWriteClient client; private ProtoSchema writerSchema = null; private long maxInflightRequest = DEFAULT_MAX_INFLIGHT_REQUESTS; private long maxInflightBytes = DEFAULT_MAX_INFLIGHT_BYTES; private String endpoint = null; private TransportChannelProvider channelProvider = null; private CredentialsProvider credentialsProvider = null; private ExecutorProvider executorProvider = null; private FlowController.LimitExceededBehavior limitExceededBehavior = FlowController.LimitExceededBehavior.Block; private String traceId = null; private TableSchema updatedTableSchema = null; private String location = null; private boolean enableConnectionPool = false; private java.time.Duration maxRetryDuration = Duration.ofMinutes(5); private String compressorName = null; // Default missing value interpretation value. private AppendRowsRequest.MissingValueInterpretation defaultMissingValueInterpretation = MissingValueInterpretation.MISSING_VALUE_INTERPRETATION_UNSPECIFIED; private RetrySettings retrySettings = null; private Builder(String streamName) { this.streamName = Preconditions.checkNotNull(streamName); this.client = null; } private Builder(String streamName, BigQueryWriteClient client) { this.streamName = Preconditions.checkNotNull(streamName); this.client = Preconditions.checkNotNull(client); } /** Sets the proto schema of the rows. */ public Builder setWriterSchema(ProtoSchema writerSchema) { this.writerSchema = writerSchema; return this; } public Builder setMaxInflightRequests(long value) { this.maxInflightRequest = value; return this; } public Builder setMaxInflightBytes(long value) { this.maxInflightBytes = value; return this; } /** Gives the ability to override the gRPC endpoint. */ public Builder setEndpoint(String endpoint) { this.endpoint = Preconditions.checkNotNull(endpoint, "Endpoint is null."); return this; } /** * Enable multiplexing for this writer. In multiplexing mode tables will share the same * connection if possible until the connection is overwhelmed. * * @param enableConnectionPool * @return Builder */ public Builder setEnableConnectionPool(boolean enableConnectionPool) { this.enableConnectionPool = enableConnectionPool; return this; } /** * {@code ChannelProvider} to use to create Channels, which must point at Cloud BigQuery Storage * API endpoint. * *

For performance, this client benefits from having multiple underlying connections. See * {@link com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.Builder#setPoolSize(int)}. */ public Builder setChannelProvider(TransportChannelProvider channelProvider) { this.channelProvider = Preconditions.checkNotNull(channelProvider, "ChannelProvider is null."); return this; } /** {@code CredentialsProvider} to use to create Credentials to authenticate calls. */ public Builder setCredentialsProvider(CredentialsProvider credentialsProvider) { this.credentialsProvider = Preconditions.checkNotNull(credentialsProvider, "CredentialsProvider is null."); return this; } /** {@code ExecutorProvider} to use to create Executor to run background jobs. */ public Builder setExecutorProvider(ExecutorProvider executorProvider) { this.executorProvider = Preconditions.checkNotNull(executorProvider, "ExecutorProvider is null."); return this; } /** * Sets traceId for debuging purpose. TraceId must follow the format of * CustomerDomain:DebugString, e.g. DATAFLOW:job_id_x. */ public Builder setTraceId(String traceId) { int colonIndex = traceId.indexOf(':'); if (colonIndex == -1 || colonIndex == 0 || colonIndex == traceId.length() - 1) { throw new IllegalArgumentException( "TraceId must follow the format of A:B. Actual:" + traceId); } this.traceId = traceId; return this; } /** Location of the table this stream writer is targeting. */ public Builder setLocation(String location) { this.location = location; return this; } /** * Sets the limit exceeded behavior. * * @param limitExceededBehavior * @return */ public Builder setLimitExceededBehavior( FlowController.LimitExceededBehavior limitExceededBehavior) throws StatusRuntimeException { if (limitExceededBehavior == FlowController.LimitExceededBehavior.Ignore) { throw new StatusRuntimeException( Status.fromCode(Code.INVALID_ARGUMENT) .withDescription("LimitExceededBehavior.Ignore is not supported on StreamWriter.")); } this.limitExceededBehavior = limitExceededBehavior; return this; } /* * Max duration to retry on retryable errors. Default is 5 minutes. You can allow unlimited * retry by setting the value to be 0. */ public Builder setMaxRetryDuration(java.time.Duration maxRetryDuration) { this.maxRetryDuration = maxRetryDuration; return this; } public Builder setCompressorName(String compressorName) { Preconditions.checkNotNull(compressorName); Preconditions.checkArgument( compressorName.equals("gzip"), "Compression of type \"%s\" isn't supported, only \"gzip\" compression is supported.", compressorName); this.compressorName = compressorName; return this; } /** * Sets the default missing value interpretation value if the column is not presented in the * missing_value_interpretations map. */ public Builder setDefaultMissingValueInterpretation( AppendRowsRequest.MissingValueInterpretation defaultMissingValueInterpretation) { this.defaultMissingValueInterpretation = defaultMissingValueInterpretation; return this; } /** * Enable client lib automatic retries on request level errors. * *

     * Immeidate Retry code:
     * ABORTED, UNAVAILABLE, CANCELLED, INTERNAL, DEADLINE_EXCEEDED
     * Backoff Retry code:
     * RESOURCE_EXHAUSTED
     *
     * Example:
     * RetrySettings retrySettings = RetrySettings.newBuilder()
     *      .setInitialRetryDelay(Duration.ofMillis(500)) // applies to backoff retry
     *      .setRetryDelayMultiplier(1.1) // applies to backoff retry
     *      .setMaxAttempts(5) // applies to both retries
     *      .setMaxRetryDelay(Duration.ofMinutes(1)) // applies to backoff retry .build();
     * 
* * @param retrySettings * @return */ public Builder setRetrySettings(RetrySettings retrySettings) { this.retrySettings = retrySettings; return this; } /** Builds the {@code StreamWriterV2}. */ public StreamWriter build() throws IOException { return new StreamWriter(this); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy