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 java.io.Closeable;
20  import org.apache.commons.logging.Log;
21  import org.apache.commons.logging.LogFactory;
22  import org.apache.hadoop.conf.Configuration;
23  import org.apache.hadoop.hbase.HConstants; // Needed for write rpc timeout
24  import org.apache.hadoop.hbase.TableName;
25  import org.apache.hadoop.hbase.classification.InterfaceAudience;
26  import org.apache.hadoop.hbase.classification.InterfaceStability;
27  import org.apache.hadoop.hbase.ipc.RpcControllerFactory;
28  
29  import java.io.IOException;
30  import java.io.InterruptedIOException;
31  import java.util.Arrays;
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.NoSuchElementException;
35  import java.util.concurrent.ConcurrentLinkedQueue;
36  import java.util.concurrent.ExecutorService;
37  import java.util.concurrent.TimeUnit;
38  import java.util.concurrent.atomic.AtomicInteger;
39  import java.util.concurrent.atomic.AtomicLong;
40  
41  /**
42   * <p>
43   * Used to communicate with a single HBase table similar to {@link HTable}
44   * but meant for batched, potentially asynchronous puts. Obtain an instance from
45   * a {@link Connection} and call {@link #close()} afterwards.
46   * </p>
47   *
48   * <p>
49   * While this can be used accross threads, great care should be used when doing so.
50   * Errors are global to the buffered mutator and the Exceptions can be thrown on any
51   * thread that causes the flush for requests.
52   * </p>
53   *
54   * @see ConnectionFactory
55   * @see Connection
56   * @since 1.0.0
57   */
58  @InterfaceAudience.Private
59  @InterfaceStability.Evolving
60  public class BufferedMutatorImpl implements BufferedMutator {
61  
62    private static final Log LOG = LogFactory.getLog(BufferedMutatorImpl.class);
63  
64    private final ExceptionListener listener;
65  
66    protected ClusterConnection connection; // non-final so can be overridden in test
67    private final TableName tableName;
68    private volatile Configuration conf;
69    @VisibleForTesting
70    final ConcurrentLinkedQueue<Mutation> writeAsyncBuffer = new ConcurrentLinkedQueue<Mutation>();
71    @VisibleForTesting
72    AtomicLong currentWriteBufferSize = new AtomicLong(0);
73  
74    /**
75     * Count the size of {@link BufferedMutatorImpl#writeAsyncBuffer}.
76     * The {@link ConcurrentLinkedQueue#size()} is NOT a constant-time operation.
77     */
78    @VisibleForTesting
79    AtomicInteger undealtMutationCount = new AtomicInteger(0);
80    private long writeBufferSize;
81    private final int maxKeyValueSize;
82    private boolean closed = false;
83    private final ExecutorService pool;
84    private int writeRpcTimeout; // needed to pass in through AsyncProcess constructor
85    private int operationTimeout;
86  
87    @VisibleForTesting
88    protected AsyncProcess ap; // non-final so can be overridden in test
89  
90    BufferedMutatorImpl(ClusterConnection conn, RpcRetryingCallerFactory rpcCallerFactory,
91        RpcControllerFactory rpcFactory, BufferedMutatorParams params) {
92      if (conn == null || conn.isClosed()) {
93        throw new IllegalArgumentException("Connection is null or closed.");
94      }
95  
96      this.tableName = params.getTableName();
97      this.connection = conn;
98      this.conf = connection.getConfiguration();
99      this.pool = params.getPool();
100     this.listener = params.getListener();
101 
102     ConnectionConfiguration tableConf = new ConnectionConfiguration(conf);
103     this.writeBufferSize = params.getWriteBufferSize() != BufferedMutatorParams.UNSET ?
104         params.getWriteBufferSize() : tableConf.getWriteBufferSize();
105     this.maxKeyValueSize = params.getMaxKeyValueSize() != BufferedMutatorParams.UNSET ?
106         params.getMaxKeyValueSize() : tableConf.getMaxKeyValueSize();
107 
108     this.writeRpcTimeout = conn.getConfiguration().getInt(HConstants.HBASE_RPC_WRITE_TIMEOUT_KEY,
109         conn.getConfiguration().getInt(HConstants.HBASE_RPC_TIMEOUT_KEY,
110             HConstants.DEFAULT_HBASE_RPC_TIMEOUT));
111     this.operationTimeout = conn.getConfiguration().getInt(
112         HConstants.HBASE_CLIENT_OPERATION_TIMEOUT,
113         HConstants.DEFAULT_HBASE_CLIENT_OPERATION_TIMEOUT);
114     // puts need to track errors globally due to how the APIs currently work.
115     ap = new AsyncProcess(connection, conf, pool, rpcCallerFactory, true, rpcFactory, writeRpcTimeout);
116   }
117 
118   @Override
119   public TableName getName() {
120     return tableName;
121   }
122 
123   @Override
124   public Configuration getConfiguration() {
125     return conf;
126   }
127 
128   @Override
129   public void mutate(Mutation m) throws InterruptedIOException,
130       RetriesExhaustedWithDetailsException {
131     mutate(Arrays.asList(m));
132   }
133 
134   @Override
135   public void mutate(List<? extends Mutation> ms) throws InterruptedIOException,
136       RetriesExhaustedWithDetailsException {
137 
138     if (closed) {
139       throw new IllegalStateException("Cannot put when the BufferedMutator is closed.");
140     }
141 
142     long toAddSize = 0;
143     int toAddCount = 0;
144     for (Mutation m : ms) {
145       if (m instanceof Put) {
146         validatePut((Put) m);
147       }
148       toAddSize += m.heapSize();
149       ++toAddCount;
150     }
151 
152     // This behavior is highly non-intuitive... it does not protect us against
153     // 94-incompatible behavior, which is a timing issue because hasError, the below code
154     // and setter of hasError are not synchronized. Perhaps it should be removed.
155     if (ap.hasError()) {
156       currentWriteBufferSize.addAndGet(toAddSize);
157       writeAsyncBuffer.addAll(ms);
158       undealtMutationCount.addAndGet(toAddCount);
159       backgroundFlushCommits(true);
160     } else {
161       currentWriteBufferSize.addAndGet(toAddSize);
162       writeAsyncBuffer.addAll(ms);
163       undealtMutationCount.addAndGet(toAddCount);
164     }
165 
166     // Now try and queue what needs to be queued.
167     while (undealtMutationCount.get() != 0
168         && currentWriteBufferSize.get() > writeBufferSize) {
169       backgroundFlushCommits(false);
170     }
171   }
172 
173   // validate for well-formedness
174   public void validatePut(final Put put) throws IllegalArgumentException {
175     HTable.validatePut(put, maxKeyValueSize);
176   }
177 
178   @Override
179   public synchronized void close() throws IOException {
180     try {
181       if (this.closed) {
182         return;
183       }
184       // As we can have an operation in progress even if the buffer is empty, we call
185       // backgroundFlushCommits at least one time.
186       backgroundFlushCommits(true);
187       this.pool.shutdown();
188       boolean terminated;
189       int loopCnt = 0;
190       do {
191         // wait until the pool has terminated
192         terminated = this.pool.awaitTermination(60, TimeUnit.SECONDS);
193         loopCnt += 1;
194         if (loopCnt >= 10) {
195           LOG.warn("close() failed to terminate pool after 10 minutes. Abandoning pool.");
196           break;
197         }
198       } while (!terminated);
199 
200     } catch (InterruptedException e) {
201       LOG.warn("waitForTermination interrupted");
202 
203     } finally {
204       this.closed = true;
205     }
206   }
207 
208   @Override
209   public synchronized void flush() throws InterruptedIOException,
210       RetriesExhaustedWithDetailsException {
211     // As we can have an operation in progress even if the buffer is empty, we call
212     // backgroundFlushCommits at least one time.
213     backgroundFlushCommits(true);
214   }
215 
216   /**
217    * Send the operations in the buffer to the servers. Does not wait for the server's answer. If
218    * the is an error (max retried reach from a previous flush or bad operation), it tries to send
219    * all operations in the buffer and sends an exception.
220    *
221    * @param synchronous - if true, sends all the writes and wait for all of them to finish before
222    *        returning.
223    */
224   private void backgroundFlushCommits(boolean synchronous) throws
225       InterruptedIOException,
226       RetriesExhaustedWithDetailsException {
227     if (!synchronous && writeAsyncBuffer.isEmpty()) {
228       return;
229     }
230 
231     if (!synchronous) {
232       try (QueueRowAccess taker = createQueueRowAccess()){
233         ap.submit(tableName, taker, true, null, false);
234         if (ap.hasError()) {
235           LOG.debug(tableName + ": One or more of the operations have failed -"
236               + " waiting for all operation in progress to finish (successfully or not)");
237         }
238       }
239     }
240     if (synchronous || ap.hasError()) {
241       while (true) {
242         try (QueueRowAccess taker = createQueueRowAccess()){
243           if (taker.isEmpty()) {
244             break;
245           }
246           ap.submit(tableName, taker, true, null, false);
247         }
248       }
249 
250       RetriesExhaustedWithDetailsException error =
251           ap.waitForAllPreviousOpsAndReset(null, tableName.getNameAsString());
252       if (error != null) {
253         if (listener == null) {
254           throw error;
255         } else {
256           this.listener.onException(error, this);
257         }
258       }
259     }
260   }
261 
262   /**
263    * This is used for legacy purposes in {@link HTable#setWriteBufferSize(long)} only. This ought
264    * not be called for production uses.
265    * @deprecated Going away when we drop public support for {@link HTableInterface}.
266    */
267   @Deprecated
268   public void setWriteBufferSize(long writeBufferSize) throws RetriesExhaustedWithDetailsException,
269       InterruptedIOException {
270     this.writeBufferSize = writeBufferSize;
271     if (currentWriteBufferSize.get() > writeBufferSize) {
272       flush();
273     }
274   }
275 
276   /**
277    * {@inheritDoc}
278    */
279   @Override
280   public long getWriteBufferSize() {
281     return this.writeBufferSize;
282   }
283 
284   public void setRpcTimeout(int writeRpcTimeout) {
285     this.writeRpcTimeout = writeRpcTimeout;
286     this.ap.setRpcTimeout(writeRpcTimeout);
287   }
288 
289   public void setOperationTimeout(int operationTimeout) {
290     this.operationTimeout = operationTimeout;
291     this.ap.setOperationTimeout(operationTimeout);
292   }
293 
294   /**
295    * This is used for legacy purposes in {@link HTable#getWriteBuffer()} only. This should not beÓ
296    * called from production uses.
297    * @deprecated Going away when we drop public support for {@link HTableInterface}.
298 Ó   */
299   @Deprecated
300   public List<Row> getWriteBuffer() {
301     return Arrays.asList(writeAsyncBuffer.toArray(new Row[0]));
302   }
303 
304   @VisibleForTesting
305   QueueRowAccess createQueueRowAccess() {
306     return new QueueRowAccess();
307   }
308 
309   @VisibleForTesting
310   class QueueRowAccess implements RowAccess<Row>, Closeable {
311     private int remainder = undealtMutationCount.getAndSet(0);
312     private Mutation last = null;
313 
314     @Override
315     public Iterator<Row> iterator() {
316       return new Iterator<Row>() {
317         private int countDown = remainder;
318         @Override
319         public boolean hasNext() {
320           return countDown > 0;
321         }
322         @Override
323         public Row next() {
324           restoreLastMutation();
325           if (!hasNext()) {
326             throw new NoSuchElementException();
327           }
328           last = writeAsyncBuffer.poll();
329           if (last == null) {
330             throw new NoSuchElementException();
331           }
332           currentWriteBufferSize.addAndGet(-last.heapSize());
333           --countDown;
334           return last;
335         }
336         @Override
337         public void remove() {
338           if (last == null) {
339             throw new IllegalStateException();
340           }
341           --remainder;
342           last = null;
343         }
344       };
345     }
346 
347     private void restoreLastMutation() {
348       // restore the last mutation since it isn't submitted
349       if (last != null) {
350         writeAsyncBuffer.add(last);
351         currentWriteBufferSize.addAndGet(last.heapSize());
352         last = null;
353       }
354     }
355 
356     @Override
357     public int size() {
358       return remainder;
359     }
360 
361     @Override
362     public boolean isEmpty() {
363       return remainder <= 0;
364     }
365     @Override
366     public void close() {
367       restoreLastMutation();
368       if (remainder > 0) {
369         undealtMutationCount.addAndGet(remainder);
370         remainder = 0;
371       }
372     }
373   }
374 }