View Javadoc

1   /**
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
4    * agreements. See the NOTICE file distributed with this work for additional information regarding
5    * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
6    * "License"); you may not use this file except in compliance with the License. You may obtain a
7    * copy of the License at
8    *
9    *  http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software distributed under the
12   * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
13   * express or implied. See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.apache.hadoop.hbase.client;
17  
18  import com.google.common.annotations.VisibleForTesting;
19  import org.apache.commons.logging.Log;
20  import org.apache.commons.logging.LogFactory;
21  import org.apache.hadoop.conf.Configuration;
22  import org.apache.hadoop.hbase.HConstants;
23  import org.apache.hadoop.hbase.TableName;
24  import org.apache.hadoop.hbase.classification.InterfaceAudience;
25  import org.apache.hadoop.hbase.classification.InterfaceStability;
26  import org.apache.hadoop.hbase.ipc.RpcControllerFactory;
27  
28  import java.io.IOException;
29  import java.io.InterruptedIOException;
30  import java.util.Arrays;
31  import java.util.LinkedList;
32  import java.util.List;
33  import java.util.concurrent.ConcurrentLinkedQueue;
34  import java.util.concurrent.ExecutorService;
35  import java.util.concurrent.TimeUnit;
36  import java.util.concurrent.atomic.AtomicLong;
37  
38  /**
39   * <p>
40   * Used to communicate with a single HBase table similar to {@link HTable}
41   * but meant for batched, potentially asynchronous puts. Obtain an instance from
42   * a {@link Connection} and call {@link #close()} afterwards.
43   * </p>
44   *
45   * <p>
46   * While this can be used accross threads, great care should be used when doing so.
47   * Errors are global to the buffered mutator and the Exceptions can be thrown on any
48   * thread that causes the flush for requests.
49   * </p>
50   *
51   * @see ConnectionFactory
52   * @see Connection
53   * @since 1.0.0
54   */
55  @InterfaceAudience.Private
56  @InterfaceStability.Evolving
57  public class BufferedMutatorImpl implements BufferedMutator {
58  
59    private static final Log LOG = LogFactory.getLog(BufferedMutatorImpl.class);
60    
61    private final ExceptionListener listener;
62  
63    protected ClusterConnection connection; // non-final so can be overridden in test
64    private final TableName tableName;
65    private volatile Configuration conf;
66    @VisibleForTesting
67    final ConcurrentLinkedQueue<Mutation> writeAsyncBuffer = new ConcurrentLinkedQueue<Mutation>();
68    @VisibleForTesting
69    AtomicLong currentWriteBufferSize = new AtomicLong(0);
70  
71    private long writeBufferSize;
72    private final int maxKeyValueSize;
73    private boolean closed = false;
74    private final ExecutorService pool;
75    private int rpcTimeout;
76    private int operationTimeout;
77  
78    @VisibleForTesting
79    protected AsyncProcess ap; // non-final so can be overridden in test
80  
81    BufferedMutatorImpl(ClusterConnection conn, RpcRetryingCallerFactory rpcCallerFactory,
82        RpcControllerFactory rpcFactory, BufferedMutatorParams params) {
83      if (conn == null || conn.isClosed()) {
84        throw new IllegalArgumentException("Connection is null or closed.");
85      }
86  
87      this.tableName = params.getTableName();
88      this.connection = conn;
89      this.conf = connection.getConfiguration();
90      this.pool = params.getPool();
91      this.listener = params.getListener();
92  
93      ConnectionConfiguration tableConf = new ConnectionConfiguration(conf);
94      this.writeBufferSize = params.getWriteBufferSize() != BufferedMutatorParams.UNSET ?
95          params.getWriteBufferSize() : tableConf.getWriteBufferSize();
96      this.maxKeyValueSize = params.getMaxKeyValueSize() != BufferedMutatorParams.UNSET ?
97          params.getMaxKeyValueSize() : tableConf.getMaxKeyValueSize();
98      this.rpcTimeout = conn.getConfiguration().getInt(HConstants.HBASE_RPC_TIMEOUT_KEY,
99              HConstants.DEFAULT_HBASE_RPC_TIMEOUT);
100     this.operationTimeout = conn.getConfiguration().getInt(
101         HConstants.HBASE_CLIENT_OPERATION_TIMEOUT,
102                  HConstants.DEFAULT_HBASE_CLIENT_OPERATION_TIMEOUT);
103     // puts need to track errors globally due to how the APIs currently work.
104     ap = new AsyncProcess(connection, conf, pool, rpcCallerFactory, true, rpcFactory, rpcTimeout);
105   }
106 
107   @Override
108   public TableName getName() {
109     return tableName;
110   }
111 
112   @Override
113   public Configuration getConfiguration() {
114     return conf;
115   }
116 
117   @Override
118   public void mutate(Mutation m) throws InterruptedIOException,
119       RetriesExhaustedWithDetailsException {
120     mutate(Arrays.asList(m));
121   }
122 
123   @Override
124   public void mutate(List<? extends Mutation> ms) throws InterruptedIOException,
125       RetriesExhaustedWithDetailsException {
126 
127     if (closed) {
128       throw new IllegalStateException("Cannot put when the BufferedMutator is closed.");
129     }
130 
131     long toAddSize = 0;
132     for (Mutation m : ms) {
133       if (m instanceof Put) {
134         validatePut((Put) m);
135       }
136       toAddSize += m.heapSize();
137     }
138 
139     // This behavior is highly non-intuitive... it does not protect us against
140     // 94-incompatible behavior, which is a timing issue because hasError, the below code
141     // and setter of hasError are not synchronized. Perhaps it should be removed.
142     if (ap.hasError()) {
143       currentWriteBufferSize.addAndGet(toAddSize);
144       writeAsyncBuffer.addAll(ms);
145       backgroundFlushCommits(true);
146     } else {
147       currentWriteBufferSize.addAndGet(toAddSize);
148       writeAsyncBuffer.addAll(ms);
149     }
150 
151     // Now try and queue what needs to be queued.
152     while (currentWriteBufferSize.get() > writeBufferSize) {
153       backgroundFlushCommits(false);
154     }
155   }
156 
157   // validate for well-formedness
158   public void validatePut(final Put put) throws IllegalArgumentException {
159     HTable.validatePut(put, maxKeyValueSize);
160   }
161 
162   @Override
163   public synchronized void close() throws IOException {
164     try {
165       if (this.closed) {
166         return;
167       }
168       // As we can have an operation in progress even if the buffer is empty, we call
169       // backgroundFlushCommits at least one time.
170       backgroundFlushCommits(true);
171       this.pool.shutdown();
172       boolean terminated;
173       int loopCnt = 0;
174       do {
175         // wait until the pool has terminated
176         terminated = this.pool.awaitTermination(60, TimeUnit.SECONDS);
177         loopCnt += 1;
178         if (loopCnt >= 10) {
179           LOG.warn("close() failed to terminate pool after 10 minutes. Abandoning pool.");
180           break;
181         }
182       } while (!terminated);
183 
184     } catch (InterruptedException e) {
185       LOG.warn("waitForTermination interrupted");
186 
187     } finally {
188       this.closed = true;
189     }
190   }
191 
192   @Override
193   public synchronized void flush() throws InterruptedIOException,
194       RetriesExhaustedWithDetailsException {
195     // As we can have an operation in progress even if the buffer is empty, we call
196     // backgroundFlushCommits at least one time.
197     backgroundFlushCommits(true);
198   }
199 
200   /**
201    * Send the operations in the buffer to the servers. Does not wait for the server's answer. If
202    * the is an error (max retried reach from a previous flush or bad operation), it tries to send
203    * all operations in the buffer and sends an exception.
204    *
205    * @param synchronous - if true, sends all the writes and wait for all of them to finish before
206    *        returning.
207    */
208   private void backgroundFlushCommits(boolean synchronous) throws
209       InterruptedIOException,
210       RetriesExhaustedWithDetailsException {
211 
212     LinkedList<Mutation> buffer = new LinkedList<>();
213     // Keep track of the size so that this thread doesn't spin forever
214     long dequeuedSize = 0;
215 
216     try {
217       // Grab all of the available mutations.
218       Mutation m;
219 
220       // If there's no buffer size drain everything. If there is a buffersize drain up to twice
221       // that amount. This should keep the loop from continually spinning if there are threads
222       // that keep adding more data to the buffer.
223       while (
224           (writeBufferSize <= 0 || dequeuedSize < (writeBufferSize * 2) || synchronous)
225               && (m = writeAsyncBuffer.poll()) != null) {
226         buffer.add(m);
227         long size = m.heapSize();
228         dequeuedSize += size;
229         currentWriteBufferSize.addAndGet(-size);
230       }
231 
232       if (!synchronous && dequeuedSize == 0) {
233         return;
234       }
235 
236       if (!synchronous) {
237         ap.submit(tableName, buffer, true, null, false);
238         if (ap.hasError()) {
239           LOG.debug(tableName + ": One or more of the operations have failed -"
240               + " waiting for all operation in progress to finish (successfully or not)");
241         }
242       }
243       if (synchronous || ap.hasError()) {
244         while (!buffer.isEmpty()) {
245           ap.submit(tableName, buffer, true, null, false);
246         }
247         RetriesExhaustedWithDetailsException error =
248             ap.waitForAllPreviousOpsAndReset(null, tableName.getNameAsString());
249         if (error != null) {
250           if (listener == null) {
251             throw error;
252           } else {
253             this.listener.onException(error, this);
254           }
255         }
256       }
257     } finally {
258       for (Mutation mut : buffer) {
259         long size = mut.heapSize();
260         currentWriteBufferSize.addAndGet(size);
261         dequeuedSize -= size;
262         writeAsyncBuffer.add(mut);
263       }
264     }
265   }
266 
267   /**
268    * This is used for legacy purposes in {@link HTable#setWriteBufferSize(long)} only. This ought
269    * not be called for production uses.
270    * @deprecated Going away when we drop public support for {@link HTableInterface}.
271    */
272   @Deprecated
273   public void setWriteBufferSize(long writeBufferSize) throws RetriesExhaustedWithDetailsException,
274       InterruptedIOException {
275     this.writeBufferSize = writeBufferSize;
276     if (currentWriteBufferSize.get() > writeBufferSize) {
277       flush();
278     }
279   }
280 
281   /**
282    * {@inheritDoc}
283    */
284   @Override
285   public long getWriteBufferSize() {
286     return this.writeBufferSize;
287   }
288 
289   public void setRpcTimeout(int rpcTimeout) {
290     this.rpcTimeout = rpcTimeout;
291     this.ap.setRpcTimeout(rpcTimeout);
292   }
293 
294   public void setOperationTimeout(int operationTimeout) {
295     this.operationTimeout = operationTimeout;
296     this.ap.setOperationTimeout(operationTimeout);
297   }
298 
299   /**
300    * This is used for legacy purposes in {@link HTable#getWriteBuffer()} only. This should not beÓ
301    * called from production uses.
302    * @deprecated Going away when we drop public support for {@link HTableInterface}.
303 Ó   */
304   @Deprecated
305   public List<Row> getWriteBuffer() {
306     return Arrays.asList(writeAsyncBuffer.toArray(new Row[0]));
307   }
308 }