public synchronized void recycleTable(long dbId, Table table, boolean recoverable) {
        if (idToTableInfo.row(dbId).containsKey(table.getId())) {
            LOG.error("table[{}-{}] already in recycle bin.", table.getId(), table.getName());
            return;
        }
        String tableName = table.getName();
        RecycleTableInfo newTableInfo = new RecycleTableInfo(dbId, table, recoverable);
        RecycleTableInfo oldTableInfo = nameToTableInfo.get(dbId, tableName);

        // There's already a table with the same name in the recycle bin.
        if (oldTableInfo != null) {
            // Our serialization rules require `idToTableInfo` and `nameToTableInfo` to be the same size, so
            // we need to make sure that tables with different ids have different names (in the recycle bin), so
            // here we rename the previous table's name.
            Table oldTable = oldTableInfo.getTable();
            oldTable.setName(uniqueIllegalTableName(tableName, oldTable.getId()));
            // Mark the renamed table as unrecoverable, as if it had been force dropped, to maintain a similar
            // behavior as older versions.
            oldTableInfo.setRecoverable(false);
            nameToTableInfo.remove(dbId, table.getName());
            nameToTableInfo.put(dbId, oldTable.getName(), oldTableInfo);
            // Speed up the deletion of this renamed table by modifying its recycle time to zero
            idToRecycleTime.put(oldTable.getId(), 0L);
        }

        // If the table was force dropped, set recycle time to zero so that this table will be deleted immediately
        // in the next cleanup round.
        idToRecycleTime.put(table.getId(), !recoverable ? 0 : System.currentTimeMillis());
        idToTableInfo.put(dbId, table.getId(), newTableInfo);
        nameToTableInfo.put(dbId, table.getName(), newTableInfo);

        LOG.info("Finished put table '{}' to recycle bin. tableId: {}", table.getName(), table.getId());
    }
@Test
    public void testRecycleTable(@Mocked GlobalStateMgr globalStateMgr, @Mocked EditLog editLog) {
        Table table1 = new Table(111, "uno", Table.TableType.VIEW, null);
        Table table2SameName = new Table(22, "dos", Table.TableType.VIEW, null);
        Table table2 = new Table(222, "dos", Table.TableType.VIEW, null);

        new Expectations() {
            {
                GlobalStateMgr.getCurrentState();
                minTimes = 0;
                result = globalStateMgr;
            }
        };
        new Expectations() {
            {
                globalStateMgr.getEditLog();
                minTimes = 0;
                result = editLog;
            }
        };
        new Expectations() {
            {
                editLog.logEraseMultiTables((List<Long>) any);
                minTimes = 0;
                result = null;
            }
        };

        // 1. add 2 tables
        long dbId = 1;
        CatalogRecycleBin recycleBin = new CatalogRecycleBin();
        recycleBin.recycleTable(dbId, table1, true);
        recycleBin.recycleTable(dbId, table2SameName, true);
        recycleBin.recycleTable(dbId, table2, true);

        Assert.assertEquals(recycleBin.getTables(dbId), Arrays.asList(table1, table2SameName, table2));
        Assert.assertSame(recycleBin.getTable(dbId, table1.getId()), table1);
        Assert.assertSame(recycleBin.getTable(dbId, table2.getId()), table2);
        Assert.assertTrue(recycleBin.idToRecycleTime.containsKey(table1.getId()));
        Assert.assertTrue(recycleBin.idToRecycleTime.containsKey(table2.getId()));

        // 2. manually set table expire time & recycle table1
        Config.catalog_trash_expire_second = 3600;
        long now = System.currentTimeMillis();
        long expireFromNow = now - 3600 * 1000L;
        recycleBin.idToRecycleTime.put(table1.getId(), expireFromNow - 1000);
        recycleBin.eraseTable(now);

        Assert.assertEquals(recycleBin.getTables(dbId), List.of(table2));
        Assert.assertNull(recycleBin.getTable(dbId, table1.getId()));
        Assert.assertSame(recycleBin.getTable(dbId, table2.getId()), table2);

        // 3. set recyle later, check if recycle now
        CatalogRecycleBin.LATE_RECYCLE_INTERVAL_SECONDS = 10;
        Assert.assertFalse(recycleBin.ensureEraseLater(table1.getId(), now));  // already erased
        Assert.assertTrue(recycleBin.ensureEraseLater(table2.getId(), now));
        Assert.assertEquals(0, recycleBin.enableEraseLater.size());
        recycleBin.idToRecycleTime.put(table2.getId(), expireFromNow + 1000);
        Assert.assertTrue(recycleBin.ensureEraseLater(table2.getId(), now));
        Assert.assertEquals(1, recycleBin.enableEraseLater.size());
        Assert.assertTrue(recycleBin.enableEraseLater.contains(table2.getId()));

        // 4. won't erase on expire time
        recycleBin.idToRecycleTime.put(table2.getId(), expireFromNow - 1000);
        recycleBin.eraseTable(now);
        Assert.assertEquals(recycleBin.getTable(dbId, table2.getId()), table2);
        Assert.assertEquals(1, recycleBin.idToRecycleTime.size());

        // 5. will erase after expire time + latency time
        recycleBin.idToRecycleTime.put(table2.getId(), expireFromNow - 11000);
        Assert.assertFalse(recycleBin.ensureEraseLater(table2.getId(), now));
        recycleBin.eraseTable(now);
        Assert.assertNull(recycleBin.getTable(dbId, table2.getId()));
        Assert.assertEquals(0, recycleBin.idToRecycleTime.size());
        Assert.assertEquals(0, recycleBin.enableEraseLater.size());
    }