2626import static org .junit .Assert .assertNotNull ;
2727import static org .junit .Assert .assertTrue ;
2828
29+ import java .io .IOException ;
2930import java .sql .Connection ;
3031import java .sql .DriverManager ;
3132import java .sql .PreparedStatement ;
3738import java .util .Map ;
3839import java .util .Properties ;
3940import java .util .Random ;
41+ import java .util .concurrent .TimeUnit ;
42+ import java .util .regex .Pattern ;
4043import org .apache .hadoop .hbase .TableName ;
4144import org .apache .hadoop .hbase .client .Admin ;
4245import org .apache .hadoop .hbase .client .Put ;
46+ import org .apache .hadoop .hbase .client .SnapshotDescription ;
4347import org .apache .hadoop .hbase .client .Table ;
48+ import org .apache .hadoop .hbase .snapshot .SnapshotCreationException ;
4449import org .apache .hadoop .hbase .util .Bytes ;
4550import org .apache .phoenix .coprocessorclient .BaseScannerRegionObserverConstants ;
4651import org .apache .phoenix .jdbc .PhoenixConnection ;
@@ -68,6 +73,79 @@ public abstract class LogicalTableNameBaseIT extends BaseTest {
6873 public static final String NEW_TABLE_PREFIX = "NEW_TBL_" ;
6974 private Properties props = PropertiesUtil .deepCopy (TEST_PROPERTIES );
7075
76+ /**
77+ * Take a snapshot, retrying if the master rejects the request because another snapshot for the
78+ * same table is already in flight. The "already running another snapshot on the same table"
79+ * rejection is most commonly produced by RPC-level retries: the original Admin.snapshot() RPC
80+ * has been accepted by the master and the snapshot procedure is in flight, but the client
81+ * retried the call and the master rejects the duplicate. In that case we poll listSnapshots()
82+ * until the snapshot we wanted appears (or until we time out and try again from scratch). This
83+ * keeps these snapshot+clone tests deterministic on slower or busy hardware.
84+ */
85+ protected static void snapshotWithRetry (Admin admin , String snapshotName , TableName tableName )
86+ throws IOException {
87+ final long alreadyRunningWaitMs = TimeUnit .MINUTES .toMillis (2L );
88+ final int maxAttempts = 5 ;
89+ final long backoffMs = 1000L ;
90+ IOException lastError = null ;
91+ for (int attempt = 1 ; attempt <= maxAttempts ; attempt ++) {
92+ try {
93+ admin .snapshot (snapshotName , tableName );
94+ return ;
95+ } catch (SnapshotCreationException e ) {
96+ if (!isAlreadyRunningSnapshot (e )) {
97+ throw e ;
98+ }
99+ if (waitForSnapshotToAppear (admin , snapshotName , alreadyRunningWaitMs )) {
100+ return ;
101+ }
102+ lastError = e ;
103+ }
104+ try {
105+ Thread .sleep (backoffMs );
106+ } catch (InterruptedException ie ) {
107+ Thread .currentThread ().interrupt ();
108+ break ;
109+ }
110+ }
111+ if (lastError != null ) {
112+ throw lastError ;
113+ }
114+ }
115+
116+ private static boolean isAlreadyRunningSnapshot (Throwable t ) {
117+ for (Throwable cur = t ; cur != null ; cur = cur .getCause ()) {
118+ String msg = cur .getMessage ();
119+ if (msg != null && msg .contains ("already running another snapshot on the same table" )) {
120+ return true ;
121+ }
122+ }
123+ return false ;
124+ }
125+
126+ private static boolean waitForSnapshotToAppear (Admin admin , String snapshotName , long timeoutMs ) {
127+ long deadline = System .currentTimeMillis () + timeoutMs ;
128+ Pattern pattern = Pattern .compile (Pattern .quote (snapshotName ));
129+ while (System .currentTimeMillis () < deadline ) {
130+ try {
131+ for (SnapshotDescription d : admin .listSnapshots (pattern )) {
132+ if (snapshotName .equals (d .getName ())) {
133+ return true ;
134+ }
135+ }
136+ } catch (IOException ioe ) {
137+ // transient; keep polling
138+ }
139+ try {
140+ Thread .sleep (500L );
141+ } catch (InterruptedException ie ) {
142+ Thread .currentThread ().interrupt ();
143+ return false ;
144+ }
145+ }
146+ return false ;
147+ }
148+
71149 static void initCluster (boolean isNamespaceMapped ) throws Exception {
72150 Map <String , String > props = Maps .newConcurrentMap ();
73151 props .put (QueryServices .DROP_METADATA_ATTRIB , Boolean .TRUE .toString ());
@@ -105,7 +183,7 @@ public static void createAndPointToNewPhysicalTable(Connection conn, String full
105183
106184 try (Admin admin = conn .unwrap (PhoenixConnection .class ).getQueryServices ().getAdmin ()) {
107185
108- admin . snapshot ( snapshotName , TableName .valueOf (fullTableHName ));
186+ snapshotWithRetry ( admin , snapshotName , TableName .valueOf (fullTableHName ));
109187 admin .cloneSnapshot (snapshotName , TableName .valueOf (fullNewTableHName ));
110188 admin .deleteSnapshot (snapshotName );
111189 LogicalTableNameIT .renameAndDropPhysicalTable (conn , null , schemaName , tableName , newTableName ,
@@ -130,7 +208,7 @@ protected HashMap<String, ArrayList<String>> testBaseTableWithIndex_BaseTableCha
130208 String fullNewTableName = SchemaUtil .getTableName (schemaName , newTableName );
131209 try (Admin admin = conn .unwrap (PhoenixConnection .class ).getQueryServices ().getAdmin ()) {
132210 String snapshotName = new StringBuilder (fullTableName ).append ("-Snapshot" ).toString ();
133- admin . snapshot ( snapshotName , TableName .valueOf (fullTableName ));
211+ snapshotWithRetry ( admin , snapshotName , TableName .valueOf (fullTableName ));
134212 admin .cloneSnapshot (snapshotName , TableName .valueOf (fullNewTableName ));
135213 admin .deleteSnapshot (snapshotName );
136214 try (Table htable = conn .unwrap (PhoenixConnection .class ).getQueryServices ()
@@ -185,7 +263,7 @@ protected HashMap<String, ArrayList<String>> test_IndexTableChange(Connection co
185263 }
186264 try (Admin admin = conn .unwrap (PhoenixConnection .class ).getQueryServices ().getAdmin ()) {
187265 String snapshotName = new StringBuilder (indexName ).append ("-Snapshot" ).toString ();
188- admin . snapshot ( snapshotName , TableName .valueOf (fullIndexTableHbaseName ));
266+ snapshotWithRetry ( admin , snapshotName , TableName .valueOf (fullIndexTableHbaseName ));
189267 admin .cloneSnapshot (snapshotName , TableName .valueOf (fullNewTableName ));
190268 admin .deleteSnapshot (snapshotName );
191269 try (Table htable = conn .unwrap (PhoenixConnection .class ).getQueryServices ()
@@ -245,7 +323,7 @@ protected HashMap<String, ArrayList<String>> testWithViewsAndIndex_BaseTableChan
245323 }
246324 try (Admin admin = conn .unwrap (PhoenixConnection .class ).getQueryServices ().getAdmin ()) {
247325 String snapshotName = new StringBuilder (fullTableName ).append ("-Snapshot" ).toString ();
248- admin . snapshot ( snapshotName , TableName .valueOf (fullTableHbaseName ));
326+ snapshotWithRetry ( admin , snapshotName , TableName .valueOf (fullTableHbaseName ));
249327 admin .cloneSnapshot (snapshotName , TableName .valueOf (fullNewTableName ));
250328 admin .deleteSnapshot (snapshotName );
251329 try (Table htable = conn .unwrap (PhoenixConnection .class ).getQueryServices ()
0 commit comments