View Javadoc

1   /**
2    * Copyright The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one or more
5    * contributor license agreements. See the NOTICE file distributed with this
6    * work for additional information regarding copyright ownership. The ASF
7    * licenses this file to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance with the License.
9    * 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, WITHOUT
15   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16   * License for the specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.hadoop.hbase.regionserver;
20  
21  import static org.junit.Assert.assertEquals;
22  import static org.junit.Assert.assertFalse;
23  import static org.junit.Assert.assertTrue;
24  import static org.mockito.Mockito.*;
25  
26  import java.io.IOException;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.List;
30  
31  import org.apache.hadoop.conf.Configuration;
32  import org.apache.hadoop.fs.FileSystem;
33  import org.apache.hadoop.fs.Path;
34  import org.apache.hadoop.hbase.Cell;
35  import org.apache.hadoop.hbase.CoordinatedStateManager;
36  import org.apache.hadoop.hbase.CoordinatedStateManagerFactory;
37  import org.apache.hadoop.hbase.HBaseTestingUtility;
38  import org.apache.hadoop.hbase.HColumnDescriptor;
39  import org.apache.hadoop.hbase.HConstants;
40  import org.apache.hadoop.hbase.HRegionInfo;
41  import org.apache.hadoop.hbase.HTableDescriptor;
42  import org.apache.hadoop.hbase.MockRegionServerServicesWithWALs;
43  import org.apache.hadoop.hbase.Server;
44  import org.apache.hadoop.hbase.ServerName;
45  import org.apache.hadoop.hbase.TableName;
46  import org.apache.hadoop.hbase.client.Durability;
47  import org.apache.hadoop.hbase.client.Put;
48  import org.apache.hadoop.hbase.client.Scan;
49  import org.apache.hadoop.hbase.testclassification.SmallTests;
50  import org.apache.hadoop.hbase.util.Bytes;
51  import org.apache.hadoop.hbase.util.FSUtils;
52  import org.apache.hadoop.hbase.wal.RegionGroupingProvider;
53  import org.apache.hadoop.hbase.wal.WALFactory;
54  import org.apache.zookeeper.KeeperException;
55  import org.junit.After;
56  import org.junit.Before;
57  import org.junit.Test;
58  import org.junit.experimental.categories.Category;
59  import org.junit.runner.RunWith;
60  import org.junit.runners.Parameterized;
61  import org.mockito.Mockito;
62  
63  import com.google.common.collect.ImmutableList;
64  
65  /**
66   * Test the {@link RegionMergeTransactionImpl} class against two HRegions (as
67   * opposed to running cluster).
68   */
69  @RunWith(Parameterized.class)
70  @Category(SmallTests.class)
71  public class TestRegionMergeTransaction {
72    private final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
73    private final Path testdir = TEST_UTIL.getDataTestDir(this.getClass()
74        .getName());
75    private HRegion region_a;
76    private HRegion region_b;
77    private HRegion region_c;
78    private WALFactory wals;
79    private FileSystem fs;
80    // Start rows of region_a,region_b,region_c
81    private static final byte[] STARTROW_A = new byte[] { 'a', 'a', 'a' };
82    private static final byte[] STARTROW_B = new byte[] { 'g', 'g', 'g' };
83    private static final byte[] STARTROW_C = new byte[] { 'w', 'w', 'w' };
84    private static final byte[] ENDROW = new byte[] { '{', '{', '{' };
85    private static final byte[] CF = HConstants.CATALOG_FAMILY;
86  
87    private MockRegionServerServicesWithWALs rsw;
88  
89    private String walProvider;
90    private String strategy;
91  
92    @Parameterized.Parameters
93    public static final Collection<Object[]> parameters() {
94      List<Object[]> params = new ArrayList<>(4);
95      params.add(new Object[] { "filesystem", "" });
96      params.add(new Object[] { "multiwal", "identity" });
97      params.add(new Object[] { "multiwal", "bounded" });
98      params.add(new Object[] { "multiwal", "namespace" });
99      return params;
100   }
101 
102   public TestRegionMergeTransaction(String walProvider, String strategy) {
103     this.walProvider = walProvider;
104     this.strategy = strategy;
105   }
106 
107   @Before
108   public void setup() throws IOException {
109     this.fs = FileSystem.get(TEST_UTIL.getConfiguration());
110     this.fs.delete(this.testdir, true);
111     TEST_UTIL.getConfiguration().set(WALFactory.WAL_PROVIDER, walProvider);
112     if (!strategy.isEmpty()) {
113       TEST_UTIL.getConfiguration().set(RegionGroupingProvider.REGION_GROUPING_STRATEGY, strategy);
114     }
115     final Configuration walConf = new Configuration(TEST_UTIL.getConfiguration());
116     FSUtils.setRootDir(walConf, this.testdir);
117     this.wals = new WALFactory(walConf, null, TestRegionMergeTransaction.class.getName());
118     ServerName sn = ServerName.valueOf("testRegionMergeTransaction", 10, 66);
119     final RegionServerServices rss = TEST_UTIL.createMockRegionServerService(sn);
120     this.rsw = new MockRegionServerServicesWithWALs(rss, wals.getWALProvider());
121     this.region_a = createRegion(this.testdir, this.wals, STARTROW_A, STARTROW_B);
122     this.region_b = createRegion(this.testdir, this.wals, STARTROW_B, STARTROW_C);
123     this.region_c = createRegion(this.testdir, this.wals, STARTROW_C, ENDROW);
124     assert region_a != null && region_b != null && region_c != null;
125     TEST_UTIL.getConfiguration().setBoolean("hbase.testing.nocluster", true);
126   }
127 
128   @After
129   public void teardown() throws IOException {
130     for (HRegion region : new HRegion[] { region_a, region_b, region_c }) {
131       if (region != null && !region.isClosed()) region.close();
132       if (region != null && this.fs.exists(region.getRegionFileSystem().getRegionDir())
133           && !this.fs.delete(region.getRegionFileSystem().getRegionDir(), true)) {
134         throw new IOException("Failed deleting of "
135             + region.getRegionFileSystem().getRegionDir());
136       }
137     }
138     if (this.wals != null) {
139       this.wals.close();
140     }
141     this.fs.delete(this.testdir, true);
142   }
143 
144   /**
145    * Test straight prepare works. Tries to merge on {@link #region_a} and
146    * {@link #region_b}
147    * @throws IOException
148    */
149   @Test
150   public void testPrepare() throws IOException {
151     prepareOnGoodRegions();
152   }
153 
154   private RegionMergeTransactionImpl prepareOnGoodRegions() throws IOException {
155     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(region_a, region_b,
156         false);
157     RegionMergeTransactionImpl spyMT = Mockito.spy(mt);
158     doReturn(false).when(spyMT).hasMergeQualifierInMeta(null,
159         region_a.getRegionInfo().getRegionName());
160     doReturn(false).when(spyMT).hasMergeQualifierInMeta(null,
161         region_b.getRegionInfo().getRegionName());
162     assertTrue(spyMT.prepare(null));
163     return spyMT;
164   }
165 
166   /**
167    * Test merging the same region
168    */
169   @Test
170   public void testPrepareWithSameRegion() throws IOException {
171     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(this.region_a,
172         this.region_a, true);
173     assertFalse("should not merge the same region even if it is forcible ",
174         mt.prepare(null));
175   }
176 
177   /**
178    * Test merging two not adjacent regions under a common merge
179    */
180   @Test
181   public void testPrepareWithRegionsNotAdjacent() throws IOException {
182     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(this.region_a,
183         this.region_c, false);
184     assertFalse("should not merge two regions if they are adjacent except it is forcible",
185         mt.prepare(null));
186   }
187 
188   /**
189    * Test merging two not adjacent regions under a compulsory merge
190    */
191   @Test
192   public void testPrepareWithRegionsNotAdjacentUnderCompulsory()
193       throws IOException {
194     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(region_a, region_c,
195         true);
196     RegionMergeTransactionImpl spyMT = Mockito.spy(mt);
197     doReturn(false).when(spyMT).hasMergeQualifierInMeta(null,
198         region_a.getRegionInfo().getRegionName());
199     doReturn(false).when(spyMT).hasMergeQualifierInMeta(null,
200         region_c.getRegionInfo().getRegionName());
201     assertTrue("Since focible is true, should merge two regions even if they are not adjacent",
202         spyMT.prepare(null));
203   }
204 
205   /**
206    * Pass a reference store
207    */
208   @Test
209   public void testPrepareWithRegionsWithReference() throws IOException {
210     HStore storeMock = Mockito.mock(HStore.class);
211     when(storeMock.hasReferences()).thenReturn(true);
212     when(storeMock.getFamily()).thenReturn(new HColumnDescriptor("cf"));
213     when(storeMock.close()).thenReturn(ImmutableList.<StoreFile>of());
214     this.region_a.stores.put(Bytes.toBytes(""), storeMock);
215     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(this.region_a,
216         this.region_b, false);
217     assertFalse(
218         "a region should not be mergeable if it has instances of store file references",
219         mt.prepare(null));
220   }
221 
222   @Test
223   public void testPrepareWithClosedRegion() throws IOException {
224     this.region_a.close();
225     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(this.region_a,
226         this.region_b, false);
227     assertFalse(mt.prepare(null));
228   }
229 
230   /**
231    * Test merging regions which are merged regions and has reference in hbase:meta all
232    * the same
233    */
234   @Test
235   public void testPrepareWithRegionsWithMergeReference() throws IOException {
236     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(region_a, region_b,
237         false);
238     RegionMergeTransactionImpl spyMT = Mockito.spy(mt);
239     doReturn(true).when(spyMT).hasMergeQualifierInMeta(null,
240         region_a.getRegionInfo().getRegionName());
241     doReturn(true).when(spyMT).hasMergeQualifierInMeta(null,
242         region_b.getRegionInfo().getRegionName());
243     assertFalse(spyMT.prepare(null));
244   }
245 
246   /**
247    * Test RegionMergeTransactionListener
248    */
249   @Test public void testRegionMergeTransactionListener() throws Exception {
250     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(region_a, region_b,
251         false);
252     RegionMergeTransactionImpl spyMT = Mockito.spy(mt);
253     doReturn(false).when(spyMT).hasMergeQualifierInMeta(null,
254         region_a.getRegionInfo().getRegionName());
255     doReturn(false).when(spyMT).hasMergeQualifierInMeta(null,
256         region_b.getRegionInfo().getRegionName());
257     RegionMergeTransaction.TransactionListener listener =
258             Mockito.mock(RegionMergeTransaction.TransactionListener.class);
259     mt.registerTransactionListener(listener);
260     mt.prepare(null);
261     TEST_UTIL.getConfiguration().setInt(HConstants.REGIONSERVER_PORT, 0);
262     CoordinatedStateManager cp = CoordinatedStateManagerFactory.getCoordinatedStateManager(
263       TEST_UTIL.getConfiguration());
264     Server mockServer = new HRegionServer(TEST_UTIL.getConfiguration(), cp);
265     mt.execute(mockServer, null);
266     verify(listener).transition(mt,
267             RegionMergeTransaction.RegionMergeTransactionPhase.STARTED,
268             RegionMergeTransaction.RegionMergeTransactionPhase.PREPARED);
269     verify(listener, times(10)).transition(any(RegionMergeTransaction.class),
270             any(RegionMergeTransaction.RegionMergeTransactionPhase.class),
271             any(RegionMergeTransaction.RegionMergeTransactionPhase.class));
272     verifyNoMoreInteractions(listener);
273   }
274 
275   @Test
276   public void testWholesomeMerge() throws IOException, InterruptedException {
277     final int rowCountOfRegionA = loadRegion(this.region_a, CF, true);
278     final int rowCountOfRegionB = loadRegion(this.region_b, CF, true);
279     assertTrue(rowCountOfRegionA > 0 && rowCountOfRegionB > 0);
280     assertEquals(rowCountOfRegionA, countRows(this.region_a));
281     assertEquals(rowCountOfRegionB, countRows(this.region_b));
282 
283     // Start transaction.
284     RegionMergeTransactionImpl mt = prepareOnGoodRegions();
285 
286     // Run the execute. Look at what it returns.
287     TEST_UTIL.getConfiguration().setInt(HConstants.REGIONSERVER_PORT, 0);
288     CoordinatedStateManager cp = CoordinatedStateManagerFactory.getCoordinatedStateManager(
289       TEST_UTIL.getConfiguration());
290     Server mockServer = new HRegionServer(TEST_UTIL.getConfiguration(), cp);
291     HRegion mergedRegion = (HRegion)mt.execute(mockServer, null);
292     // Do some assertions about execution.
293     assertTrue(this.fs.exists(mt.getMergesDir()));
294     // Assert region_a and region_b is closed.
295     assertTrue(region_a.isClosed());
296     assertTrue(region_b.isClosed());
297 
298     // Assert mergedir is empty -- because its content will have been moved out
299     // to be under the merged region dirs.
300     assertEquals(0, this.fs.listStatus(mt.getMergesDir()).length);
301     // Check merged region have correct key span.
302     assertTrue(Bytes.equals(this.region_a.getRegionInfo().getStartKey(),
303         mergedRegion.getRegionInfo().getStartKey()));
304     assertTrue(Bytes.equals(this.region_b.getRegionInfo().getEndKey(),
305         mergedRegion.getRegionInfo().getEndKey()));
306     // Count rows. merged region are already open
307     try {
308       int mergedRegionRowCount = countRows(mergedRegion);
309       assertEquals((rowCountOfRegionA + rowCountOfRegionB),
310           mergedRegionRowCount);
311     } finally {
312       mergedRegion.close();
313     }
314     // Assert the write lock is no longer held on region_a and region_b
315     assertTrue(!this.region_a.lock.writeLock().isHeldByCurrentThread());
316     assertTrue(!this.region_b.lock.writeLock().isHeldByCurrentThread());
317   }
318 
319   @Test
320   public void testRollback() throws IOException, InterruptedException {
321     final int rowCountOfRegionA = loadRegion(this.region_a, CF, true);
322     final int rowCountOfRegionB = loadRegion(this.region_b, CF, true);
323     assertTrue(rowCountOfRegionA > 0 && rowCountOfRegionB > 0);
324     assertEquals(rowCountOfRegionA, countRows(this.region_a));
325     assertEquals(rowCountOfRegionB, countRows(this.region_b));
326 
327     // Start transaction.
328     RegionMergeTransactionImpl mt = prepareOnGoodRegions();
329 
330     when(mt.createMergedRegionFromMerges(region_a, region_b,
331         mt.getMergedRegionInfo())).thenThrow(
332         new MockedFailedMergedRegionCreation());
333 
334     // Run the execute. Look at what it returns.
335     boolean expectedException = false;
336     TEST_UTIL.getConfiguration().setInt(HConstants.REGIONSERVER_PORT, 0);
337     CoordinatedStateManager cp = CoordinatedStateManagerFactory.getCoordinatedStateManager(
338       TEST_UTIL.getConfiguration());
339     Server mockServer = new HRegionServer(TEST_UTIL.getConfiguration(), cp);
340     try {
341       mt.execute(mockServer, null);
342     } catch (MockedFailedMergedRegionCreation e) {
343       expectedException = true;
344     }
345     assertTrue(expectedException);
346     // Run rollback
347     assertTrue(mt.rollback(null, null));
348 
349     // Assert I can scan region_a and region_b.
350     int rowCountOfRegionA2 = countRows(this.region_a);
351     assertEquals(rowCountOfRegionA, rowCountOfRegionA2);
352     int rowCountOfRegionB2 = countRows(this.region_b);
353     assertEquals(rowCountOfRegionB, rowCountOfRegionB2);
354 
355     // Assert rollback cleaned up stuff in fs
356     assertTrue(!this.fs.exists(FSUtils.getRegionDirFromRootDir(this.testdir,
357         mt.getMergedRegionInfo())));
358 
359     assertTrue(!this.region_a.lock.writeLock().isHeldByCurrentThread());
360     assertTrue(!this.region_b.lock.writeLock().isHeldByCurrentThread());
361 
362     // Now retry the merge but do not throw an exception this time.
363     assertTrue(mt.prepare(null));
364     HRegion mergedRegion = (HRegion)mt.execute(mockServer, null);
365     // Count rows. daughters are already open
366     // Count rows. merged region are already open
367     try {
368       int mergedRegionRowCount = countRows(mergedRegion);
369       assertEquals((rowCountOfRegionA + rowCountOfRegionB),
370           mergedRegionRowCount);
371     } finally {
372       mergedRegion.close();
373     }
374     // Assert the write lock is no longer held on region_a and region_b
375     assertTrue(!this.region_a.lock.writeLock().isHeldByCurrentThread());
376     assertTrue(!this.region_b.lock.writeLock().isHeldByCurrentThread());
377   }
378 
379   @Test
380   public void testFailAfterPONR() throws IOException, KeeperException, InterruptedException {
381     final int rowCountOfRegionA = loadRegion(this.region_a, CF, true);
382     final int rowCountOfRegionB = loadRegion(this.region_b, CF, true);
383     assertTrue(rowCountOfRegionA > 0 && rowCountOfRegionB > 0);
384     assertEquals(rowCountOfRegionA, countRows(this.region_a));
385     assertEquals(rowCountOfRegionB, countRows(this.region_b));
386 
387     // Start transaction.
388     RegionMergeTransactionImpl mt = prepareOnGoodRegions();
389     Mockito.doThrow(new MockedFailedMergedRegionOpen())
390         .when(mt)
391         .openMergedRegion((Server) Mockito.anyObject(),
392             (RegionServerServices) Mockito.anyObject(),
393             (HRegion) Mockito.anyObject());
394 
395     // Run the execute. Look at what it returns.
396     boolean expectedException = false;
397     TEST_UTIL.getConfiguration().setInt(HConstants.REGIONSERVER_PORT, 0);
398     CoordinatedStateManager cp = CoordinatedStateManagerFactory.getCoordinatedStateManager(
399       TEST_UTIL.getConfiguration());
400     Server mockServer = new HRegionServer(TEST_UTIL.getConfiguration(), cp);
401     try {
402       mt.execute(mockServer, null);
403     } catch (MockedFailedMergedRegionOpen e) {
404       expectedException = true;
405     }
406     assertTrue(expectedException);
407     // Run rollback returns false that we should restart.
408     assertFalse(mt.rollback(null, null));
409     // Make sure that merged region is still in the filesystem, that
410     // they have not been removed; this is supposed to be the case if we go
411     // past point of no return.
412     Path tableDir = this.region_a.getRegionFileSystem().getRegionDir()
413         .getParent();
414     Path mergedRegionDir = new Path(tableDir, mt.getMergedRegionInfo()
415         .getEncodedName());
416     assertTrue(TEST_UTIL.getTestFileSystem().exists(mergedRegionDir));
417   }
418 
419   @Test
420   public void testMergedRegionBoundary() {
421     TableName tableName =
422         TableName.valueOf("testMergedRegionBoundary");
423     byte[] a = Bytes.toBytes("a");
424     byte[] b = Bytes.toBytes("b");
425     byte[] z = Bytes.toBytes("z");
426     HRegionInfo r1 = new HRegionInfo(tableName);
427     HRegionInfo r2 = new HRegionInfo(tableName, a, z);
428     HRegionInfo m = RegionMergeTransactionImpl.getMergedRegionInfo(r1, r2);
429     assertTrue(Bytes.equals(m.getStartKey(), r1.getStartKey())
430         && Bytes.equals(m.getEndKey(), r1.getEndKey()));
431 
432     r1 = new HRegionInfo(tableName, null, a);
433     r2 = new HRegionInfo(tableName, a, z);
434     m = RegionMergeTransactionImpl.getMergedRegionInfo(r1, r2);
435     assertTrue(Bytes.equals(m.getStartKey(), r1.getStartKey())
436         && Bytes.equals(m.getEndKey(), r2.getEndKey()));
437 
438     r1 = new HRegionInfo(tableName, null, a);
439     r2 = new HRegionInfo(tableName, z, null);
440     m = RegionMergeTransactionImpl.getMergedRegionInfo(r1, r2);
441     assertTrue(Bytes.equals(m.getStartKey(), r1.getStartKey())
442         && Bytes.equals(m.getEndKey(), r2.getEndKey()));
443 
444     r1 = new HRegionInfo(tableName, a, z);
445     r2 = new HRegionInfo(tableName, z, null);
446     m = RegionMergeTransactionImpl.getMergedRegionInfo(r1, r2);
447     assertTrue(Bytes.equals(m.getStartKey(), r1.getStartKey())
448       && Bytes.equals(m.getEndKey(), r2.getEndKey()));
449 
450     r1 = new HRegionInfo(tableName, a, b);
451     r2 = new HRegionInfo(tableName, b, z);
452     m = RegionMergeTransactionImpl.getMergedRegionInfo(r1, r2);
453     assertTrue(Bytes.equals(m.getStartKey(), r1.getStartKey())
454       && Bytes.equals(m.getEndKey(), r2.getEndKey()));
455   }
456 
457   /**
458    * Exception used in this class only.
459    */
460   @SuppressWarnings("serial")
461   private class MockedFailedMergedRegionCreation extends IOException {
462   }
463 
464   @SuppressWarnings("serial")
465   private class MockedFailedMergedRegionOpen extends IOException {
466   }
467 
468   private HRegion createRegion(final Path testdir, final WALFactory wals,
469       final byte[] startrow, final byte[] endrow)
470       throws IOException {
471     // Make a region with start and end keys.
472     HTableDescriptor htd = new HTableDescriptor(TableName.valueOf("table"));
473     HColumnDescriptor hcd = new HColumnDescriptor(CF);
474     htd.addFamily(hcd);
475     HRegionInfo hri = new HRegionInfo(htd.getTableName(), startrow, endrow);
476     HRegion a = HRegion.createHRegion(hri, testdir,
477         TEST_UTIL.getConfiguration(), htd);
478     HRegion.closeHRegion(a);
479     return HRegion.openHRegion(testdir, hri, htd,
480       wals.getWAL(hri.getEncodedNameAsBytes(), hri.getTable().getNamespace()),
481       TEST_UTIL.getConfiguration(), rsw, null);
482   }
483 
484   private int countRows(final HRegion r) throws IOException {
485     int rowcount = 0;
486     InternalScanner scanner = r.getScanner(new Scan());
487     try {
488       List<Cell> kvs = new ArrayList<Cell>();
489       boolean hasNext = true;
490       while (hasNext) {
491         hasNext = scanner.next(kvs);
492         if (!kvs.isEmpty())
493           rowcount++;
494       }
495     } finally {
496       scanner.close();
497     }
498     return rowcount;
499   }
500 
501   /**
502    * Load region with rows from 'aaa' to 'zzz', skip the rows which are out of
503    * range of the region
504    * @param r Region
505    * @param f Family
506    * @param flush flush the cache if true
507    * @return Count of rows loaded.
508    * @throws IOException
509    */
510   private int loadRegion(final HRegion r, final byte[] f, final boolean flush)
511       throws IOException {
512     byte[] k = new byte[3];
513     int rowCount = 0;
514     for (byte b1 = 'a'; b1 <= 'z'; b1++) {
515       for (byte b2 = 'a'; b2 <= 'z'; b2++) {
516         for (byte b3 = 'a'; b3 <= 'z'; b3++) {
517           k[0] = b1;
518           k[1] = b2;
519           k[2] = b3;
520           if (!HRegion.rowIsInRange(r.getRegionInfo(), k)) {
521             continue;
522           }
523           Put put = new Put(k);
524           put.add(f, null, k);
525           if (r.getWAL() == null)
526             put.setDurability(Durability.SKIP_WAL);
527           r.put(put);
528           rowCount++;
529         }
530       }
531       if (flush) {
532         r.flush(true);
533       }
534     }
535     return rowCount;
536   }
537 
538 }