View Javadoc

1   /**
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  package org.apache.hadoop.hbase.regionserver;
20  
21  import java.text.SimpleDateFormat;
22  import java.util.Date;
23  import java.util.Map;
24  import java.util.concurrent.ConcurrentHashMap;
25  
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  import org.apache.hadoop.conf.Configuration;
29  import org.apache.hadoop.hbase.HConstants;
30  import org.apache.hadoop.hbase.ScheduledChore;
31  import org.apache.hadoop.hbase.Stoppable;
32  import org.apache.hadoop.hbase.classification.InterfaceAudience;
33  import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
34  import org.apache.hadoop.hbase.util.NonceKey;
35  
36  import com.google.common.annotations.VisibleForTesting;
37  
38  /**
39   * Implementation of nonce manager that stores nonces in a hash map and cleans them up after
40   * some time; if nonce group/client ID is supplied, nonces are stored by client ID.
41   */
42  @InterfaceAudience.Private
43  public class ServerNonceManager {
44    public static final String HASH_NONCE_GRACE_PERIOD_KEY = "hbase.server.hashNonce.gracePeriod";
45    private static final Log LOG = LogFactory.getLog(ServerNonceManager.class);
46  
47    /** The time to wait in an extremely unlikely case of a conflict with a running op.
48     * Only here so that tests could override it and not wait. */
49    private int conflictWaitIterationMs = 30000;
50  
51    private static final SimpleDateFormat tsFormat = new SimpleDateFormat("HH:mm:ss.SSS");
52  
53    // This object is used to synchronize on in case of collisions, and for cleanup.
54    private static class OperationContext {
55      static final int DONT_PROCEED = 0;
56      static final int PROCEED = 1;
57      static final int WAIT = 2;
58  
59      // 0..1 - state, 2..2 - whether anyone is waiting, 3.. - ts of last activity
60      private long data = 0;
61      private static final long STATE_BITS = 3;
62      private static final long WAITING_BIT = 4;
63      private static final long ALL_FLAG_BITS = WAITING_BIT | STATE_BITS;
64  
65      private volatile long mvcc;
66  
67      @Override
68      public String toString() {
69        return "[state " + getState() + ", hasWait " + hasWait() + ", activity "
70            + tsFormat.format(new Date(getActivityTime())) + "]";
71      }
72  
73      public OperationContext() {
74        setState(WAIT);
75        reportActivity();
76      }
77  
78      public void setState(int state) {
79        this.data = (this.data & ~STATE_BITS) | state;
80      }
81  
82      public int getState() {
83        return (int)(this.data & STATE_BITS);
84      }
85  
86      public void setHasWait() {
87        this.data = this.data | WAITING_BIT;
88      }
89  
90      public boolean hasWait() {
91        return (this.data & WAITING_BIT) == WAITING_BIT;
92      }
93  
94      public void reportActivity() {
95        long now = EnvironmentEdgeManager.currentTime();
96        this.data = (this.data & ALL_FLAG_BITS) | (now << 3);
97      }
98  
99      public boolean isExpired(long minRelevantTime) {
100       return getActivityTime() < (minRelevantTime & (~0l >>> 3));
101     }
102 
103     private long getActivityTime() {
104       return this.data >>> 3;
105     }
106 
107     public void setMvcc(long mvcc) {
108       this.mvcc = mvcc;
109     }
110 
111     public long getMvcc() {
112       return this.mvcc;
113     }
114   }
115 
116   /**
117    * Nonces.
118    * Approximate overhead per nonce: 64 bytes from hashmap, 32 from two objects (k/v),
119    * NK: 16 bytes (2 longs), OC: 8 bytes (1 long) - so, 120 bytes.
120    * With 30min expiration time, 5k increments/appends per sec., we'd use approximately 1Gb,
121    * which is a realistic worst case. If it's much worse, we could use some sort of memory
122    * limit and cleanup.
123    */
124   private ConcurrentHashMap<NonceKey, OperationContext> nonces =
125       new ConcurrentHashMap<NonceKey, OperationContext>();
126 
127   private int deleteNonceGracePeriod;
128 
129   public ServerNonceManager(Configuration conf) {
130     // Default - 30 minutes.
131     deleteNonceGracePeriod = conf.getInt(HASH_NONCE_GRACE_PERIOD_KEY, 30 * 60 * 1000);
132     if (deleteNonceGracePeriod < 60 * 1000) {
133       LOG.warn("Nonce grace period " + deleteNonceGracePeriod
134           + " is less than a minute; might be too small to be useful");
135     }
136   }
137 
138   @VisibleForTesting
139   public void setConflictWaitIterationMs(int conflictWaitIterationMs) {
140     this.conflictWaitIterationMs = conflictWaitIterationMs;
141   }
142 
143   /**
144    * Starts the operation if operation with such nonce has not already succeeded. If the
145    * operation is in progress, waits for it to end and checks whether it has succeeded.
146    * @param group Nonce group.
147    * @param nonce Nonce.
148    * @param stoppable Stoppable that terminates waiting (if any) when the server is stopped.
149    * @return true if the operation has not already succeeded and can proceed; false otherwise.
150    */
151   public boolean startOperation(long group, long nonce, Stoppable stoppable)
152       throws InterruptedException {
153     if (nonce == HConstants.NO_NONCE) return true;
154     NonceKey nk = new NonceKey(group, nonce);
155     OperationContext ctx = new OperationContext();
156     while (true) {
157       OperationContext oldResult = nonces.putIfAbsent(nk, ctx);
158       if (oldResult == null) return true;
159 
160       // Collision with some operation - should be extremely rare.
161       synchronized (oldResult) {
162         int oldState = oldResult.getState();
163         LOG.debug("Conflict detected by nonce: " + nk + ", " + oldResult);
164         if (oldState != OperationContext.WAIT) {
165           return oldState == OperationContext.PROCEED; // operation ended
166         }
167         oldResult.setHasWait();
168         oldResult.wait(this.conflictWaitIterationMs); // operation is still active... wait and loop
169         if (stoppable.isStopped()) {
170           throw new InterruptedException("Server stopped");
171         }
172       }
173     }
174   }
175 
176   /**
177    * Ends the operation started by startOperation.
178    * @param group Nonce group.
179    * @param nonce Nonce.
180    * @param success Whether the operation has succeeded.
181    */
182   public void endOperation(long group, long nonce, boolean success) {
183     if (nonce == HConstants.NO_NONCE) return;
184     NonceKey nk = new NonceKey(group, nonce);
185     OperationContext newResult = nonces.get(nk);
186     assert newResult != null;
187     synchronized (newResult) {
188       assert newResult.getState() == OperationContext.WAIT;
189       // If we failed, other retries can proceed.
190       newResult.setState(success ? OperationContext.DONT_PROCEED : OperationContext.PROCEED);
191       if (success) {
192         newResult.reportActivity(); // Set time to use for cleanup.
193       } else {
194         OperationContext val = nonces.remove(nk);
195         assert val == newResult;
196       }
197       if (newResult.hasWait()) {
198         LOG.debug("Conflict with running op ended: " + nk + ", " + newResult);
199         newResult.notifyAll();
200       }
201     }
202   }
203 
204   /**
205    * Store the write point in OperationContext when the operation succeed.
206    * @param group Nonce group.
207    * @param nonce Nonce.
208    * @param mvcc Write point of the succeed operation.
209    */
210   public void addMvccToOperationContext(long group, long nonce, long mvcc) {
211     if (nonce == HConstants.NO_NONCE) {
212       return;
213     }
214     NonceKey nk = new NonceKey(group, nonce);
215     OperationContext result = nonces.get(nk);
216     assert result != null;
217     synchronized (result) {
218       result.setMvcc(mvcc);
219     }
220   }
221 
222   /**
223    * Return the write point of the previous succeed operation.
224    * @param group Nonce group.
225    * @param nonce Nonce.
226    * @return write point of the previous succeed operation.
227    */
228   public long getMvccFromOperationContext(long group, long nonce) {
229     if (nonce == HConstants.NO_NONCE) {
230       return Long.MAX_VALUE;
231     }
232     NonceKey nk = new NonceKey(group, nonce);
233     OperationContext result = nonces.get(nk);
234     return result == null ? Long.MAX_VALUE : result.getMvcc();
235   }
236 
237   /**
238    * Reports the operation from WAL during replay.
239    * @param group Nonce group.
240    * @param nonce Nonce.
241    * @param writeTime Entry write time, used to ignore entries that are too old.
242    */
243   public void reportOperationFromWal(long group, long nonce, long writeTime) {
244     if (nonce == HConstants.NO_NONCE) return;
245     // Give the write time some slack in case the clocks are not synchronized.
246     long now = EnvironmentEdgeManager.currentTime();
247     if (now > writeTime + (deleteNonceGracePeriod * 1.5)) return;
248     OperationContext newResult = new OperationContext();
249     newResult.setState(OperationContext.DONT_PROCEED);
250     NonceKey nk = new NonceKey(group, nonce);
251     OperationContext oldResult = nonces.putIfAbsent(nk, newResult);
252     if (oldResult != null) {
253       // Some schemes can have collisions (for example, expiring hashes), so just log it.
254       // We have no idea about the semantics here, so this is the least of many evils.
255       LOG.warn("Nonce collision during WAL recovery: " + nk
256           + ", " + oldResult + " with " + newResult);
257     }
258   }
259 
260   /**
261    * Creates a scheduled chore that is used to clean up old nonces.
262    * @param stoppable Stoppable for the chore.
263    * @return ScheduledChore; the scheduled chore is not started.
264    */
265   public ScheduledChore createCleanupScheduledChore(Stoppable stoppable) {
266     // By default, it will run every 6 minutes (30 / 5).
267     return new ScheduledChore("nonceCleaner", stoppable, deleteNonceGracePeriod / 5) {
268       @Override
269       protected void chore() {
270         cleanUpOldNonces();
271       }
272     };
273   }
274 
275   private void cleanUpOldNonces() {
276     long cutoff = EnvironmentEdgeManager.currentTime() - deleteNonceGracePeriod;
277     for (Map.Entry<NonceKey, OperationContext> entry : nonces.entrySet()) {
278       OperationContext oc = entry.getValue();
279       if (!oc.isExpired(cutoff)) continue;
280       synchronized (oc) {
281         if (oc.getState() == OperationContext.WAIT || !oc.isExpired(cutoff)) continue;
282         nonces.remove(entry.getKey());
283       }
284     }
285   }
286 }