KUDU-3326 [master] add improvement on interpreting the 'reserve_seconds' field for DeleteRPC

If the field 'reserve_seconds' is specified by the client, then it's clear the delete request
coming from a newer Kudu client with the precise value for 'reserve_seconds', and the
field's value from the request should be taken as-is regardless of the current setting of the
--deleted_table_reserve_seconds flag at the server side.

Change-Id: I604caf5d7326e27acd6d626b482376fc6eed1b0f
Reviewed-on: http://gerrit.cloudera.org:8080/19047
Tested-by: Kudu Jenkins
Reviewed-by: Yingchun Lai <acelyc1112009@gmail.com>
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/AsyncKuduClient.java b/java/kudu-client/src/main/java/org/apache/kudu/client/AsyncKuduClient.java
index 50cdae3..2e8a24e 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/AsyncKuduClient.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/AsyncKuduClient.java
@@ -697,15 +697,6 @@
   }
 
   /**
-   * Delete a table with the specified name. The table is purged immediately.
-   * @param name the table's name
-   * @return a deferred object to track the progress of the deleteTable command
-   */
-  public Deferred<DeleteTableResponse> deleteTable(String name) {
-    return deleteTable(name, NO_SOFT_DELETED_STATE_RESERVED_SECONDS);
-  }
-
-  /**
    * Delete a table with the specified name.
    * @param name the table's name
    * @param reserveSeconds the soft deleted table to be alive time
@@ -723,6 +714,22 @@
   }
 
   /**
+   * Delete a table with the specified name.
+   * The behavior of DeleteRPC is controlled by the
+   * '--default_deleted_table_reserve_seconds' flag on master.
+   * @param name the table's name
+   * @return a deferred object to track the progress of the deleteTable command
+   */
+  public Deferred<DeleteTableResponse> deleteTable(String name) {
+    checkIsClosed();
+    DeleteTableRequest delete = new DeleteTableRequest(this.masterTable,
+                                                       name,
+                                                       timer,
+                                                       defaultAdminOperationTimeoutMs);
+    return sendRpcToTablet(delete);
+  }
+
+  /**
    * Recall a soft-deleted table on the cluster with the specified id
    * @param id the table's id
    * @return a deferred object to track the progress of the recall command
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/DeleteTableRequest.java b/java/kudu-client/src/main/java/org/apache/kudu/client/DeleteTableRequest.java
index 49b9ff4..5ecf13a 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/DeleteTableRequest.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/DeleteTableRequest.java
@@ -34,7 +34,7 @@
 
   private final String name;
 
-  private final int reserveSeconds;
+  private int reserveSeconds = -1;
 
   DeleteTableRequest(KuduTable table,
                      String name,
@@ -46,13 +46,23 @@
     this.reserveSeconds = reserveSeconds;
   }
 
+  DeleteTableRequest(KuduTable table,
+                     String name,
+                     Timer timer,
+                     long timeoutMillis) {
+    super(table, timer, timeoutMillis);
+    this.name = name;
+  }
+
   @Override
   Message createRequestPB() {
     final Master.DeleteTableRequestPB.Builder builder = Master.DeleteTableRequestPB.newBuilder();
     Master.TableIdentifierPB tableID =
         Master.TableIdentifierPB.newBuilder().setTableName(name).build();
     builder.setTable(tableID);
-    builder.setReserveSeconds(reserveSeconds);
+    if (reserveSeconds >= 0) {
+      builder.setReserveSeconds(reserveSeconds);
+    }
     return builder.build();
   }
 
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/KuduClient.java b/java/kudu-client/src/main/java/org/apache/kudu/client/KuduClient.java
index 382cce9..1536574 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/KuduClient.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/KuduClient.java
@@ -163,12 +163,15 @@
 
   /**
    * Delete a table on the cluster with the specified name.
+   * The deleted table may turn to soft-deleted status with the flag
+   * default_deleted_table_reserve_seconds set to nonzero on the master side.
+   *
    * @param name the table's name
    * @return an rpc response object
    * @throws KuduException if anything went wrong
    */
   public DeleteTableResponse deleteTable(String name) throws KuduException {
-    Deferred<DeleteTableResponse> d = asyncClient.deleteTable(name, 0);
+    Deferred<DeleteTableResponse> d = asyncClient.deleteTable(name);
     return joinAndHandleException(d);
   }
 
diff --git a/src/kudu/client/client-internal.cc b/src/kudu/client/client-internal.cc
index 63b5be4..ca6bd53 100644
--- a/src/kudu/client/client-internal.cc
+++ b/src/kudu/client/client-internal.cc
@@ -379,13 +379,15 @@
                                      const string& table_name,
                                      const MonoTime& deadline,
                                      bool modify_external_catalogs,
-                                     uint32_t reserve_seconds) {
+                                     std::optional<uint32_t> reserve_seconds) {
   DeleteTableRequestPB req;
   DeleteTableResponsePB resp;
 
   req.mutable_table()->set_table_name(table_name);
   req.set_modify_external_catalogs(modify_external_catalogs);
-  req.set_reserve_seconds(reserve_seconds);
+  if (reserve_seconds) {
+    req.set_reserve_seconds(*reserve_seconds);
+  }
   Synchronizer sync;
   AsyncLeaderMasterRpc<DeleteTableRequestPB, DeleteTableResponsePB> rpc(
       deadline, client, BackoffType::EXPONENTIAL, req, &resp,
diff --git a/src/kudu/client/client-internal.h b/src/kudu/client/client-internal.h
index 7d70b50..b29b77d 100644
--- a/src/kudu/client/client-internal.h
+++ b/src/kudu/client/client-internal.h
@@ -20,6 +20,7 @@
 #include <functional>
 #include <map>
 #include <memory>
+#include <optional>
 #include <set>
 #include <string>
 #include <unordered_set>
@@ -83,8 +84,6 @@
 
 class KuduClient::Data {
  public:
-  static const uint32_t kSoftDeletedTableReservationSeconds = 60 * 60 * 24 * 7;
-
   Data();
   ~Data();
 
@@ -124,7 +123,7 @@
                             const std::string& table_name,
                             const MonoTime& deadline,
                             bool modify_external_catalogs = true,
-                            uint32_t reserve_seconds = kSoftDeletedTableReservationSeconds);
+                            std::optional<uint32_t> reserve_seconds = std::nullopt);
 
   static Status RecallTable(KuduClient* client,
                             const std::string& table_id,
diff --git a/src/kudu/client/client-test.cc b/src/kudu/client/client-test.cc
index 071f88c..b7d5412 100644
--- a/src/kudu/client/client-test.cc
+++ b/src/kudu/client/client-test.cc
@@ -5102,8 +5102,8 @@
     // Not allowed to delete the soft_deleted table with new reserve_seconds value.
     s = client_->SoftDeleteTable(kTableName, 600);
     ASSERT_TRUE(s.IsInvalidArgument());
-    ASSERT_STR_CONTAINS(s.ToString(), Substitute("soft_deleted table $0 should not be deleted",
-                                                 kTableName));
+    ASSERT_STR_CONTAINS(s.ToString(),
+        Substitute("soft_deleted table $0 should not be soft deleted again", kTableName));
   }
 
   {
@@ -5355,8 +5355,14 @@
   ASSERT_OK(client_->ListTables(&tables));
   ASSERT_TRUE(tables.empty());
 
+  // The behavior of DeleteTable turn to soft-delete
+  // soft-deleted table should not be soft deleted
+  Status s = client_->DeleteTable(kTableName);
+  ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
+  ASSERT_STR_CONTAINS(s.ToString(),
+      Substitute("soft_deleted table $0 should not be soft deleted again", kTableName));
   // purge the soft-deleted table
-  ASSERT_OK(client_->DeleteTable(kTableName));
+  ASSERT_OK(client_->SoftDeleteTable(kTableName, 0));
 
   // No tables left.
   ASSERT_OK(client_->ListTables(&tables));
diff --git a/src/kudu/client/client.cc b/src/kudu/client/client.cc
index 0706277..f47f0ef 100644
--- a/src/kudu/client/client.cc
+++ b/src/kudu/client/client.cc
@@ -510,7 +510,9 @@
 }
 
 Status KuduClient::DeleteTable(const string& table_name) {
-  return SoftDeleteTable(table_name);
+  // The default param 'reserve_seconds' not be set means the behavior of DeleteRPC is
+  // controlled by the 'default_deleted_table_reserve_seconds' flag.
+  return DeleteTableInCatalogs(table_name, true);
 }
 
 Status KuduClient::SoftDeleteTable(const string& table_name,
@@ -520,10 +522,12 @@
 
 Status KuduClient::DeleteTableInCatalogs(const string& table_name,
                                          bool modify_external_catalogs,
-                                         uint32_t reserve_seconds) {
+                                         int32_t reserve_seconds) {
   MonoTime deadline = MonoTime::Now() + default_admin_operation_timeout();
+  std::optional<uint32_t> reserve_time = reserve_seconds >= 0 ?
+      std::make_optional(reserve_seconds) : std::nullopt;
   return  KuduClient::Data::DeleteTable(this, table_name, deadline, modify_external_catalogs,
-                                        reserve_seconds);
+                                        reserve_time);
 }
 
 Status KuduClient::RecallTable(const string& table_id, const string& new_table_name) {
diff --git a/src/kudu/client/client.h b/src/kudu/client/client.h
index cbe9877..834d77d 100644
--- a/src/kudu/client/client.h
+++ b/src/kudu/client/client.h
@@ -56,9 +56,9 @@
 class AlterTableTest;
 class AuthzTokenTest;
 class ClientStressTest_TestUniqueClientIds_Test;
-class MetaCacheLookupStressTest_PerfSynthetic_Test;
 class DisableWriteWhenExceedingQuotaTest;
 class KuduPartialRow;
+class MetaCacheLookupStressTest_PerfSynthetic_Test;
 class MonoDelta;
 class Partition;
 class PartitionSchema;
@@ -683,6 +683,8 @@
                                  bool* create_in_progress);
 
   /// Delete/drop a table without reserving.
+  /// The deleted table may turn to soft-deleted status with the flag
+  /// --default_deleted_table_reserve_seconds set to nonzero on the master side.
   ///
   /// The delete operation or drop operation means that the service will directly
   /// delete the table after receiving the instruction. Which means that once we
@@ -736,10 +738,12 @@
   ///   which the Kudu master has been configured to integrate with.
   /// @param [in] reserve_seconds
   ///   Reserve seconds after being deleted.
+  ///   Default value '-1' means not specified and the server will use master side config.
+  ///   '0' means purge immediately and other values means reserve some seconds.
   /// @return Operation status.
   Status DeleteTableInCatalogs(const std::string& table_name,
                                bool modify_external_catalogs,
-                               uint32_t reserve_seconds = 0) KUDU_NO_EXPORT;
+                               int32_t reserve_seconds = -1) KUDU_NO_EXPORT;
 
   /// Recall a deleted but still reserved table.
   ///
diff --git a/src/kudu/master/catalog_manager.cc b/src/kudu/master/catalog_manager.cc
index 22c56a3..916880a 100644
--- a/src/kudu/master/catalog_manager.cc
+++ b/src/kudu/master/catalog_manager.cc
@@ -2410,18 +2410,36 @@
   bool is_soft_deleted_table = false;
   bool is_expired_table = false;
   Status s = GetTableStates(req.table(), kAllTableType, &is_soft_deleted_table, &is_expired_table);
-  if (s.ok() && is_soft_deleted_table && req.reserve_seconds() != 0) {
+  // The only error is NotFound, deal it after authorize by DeleteTableRpc(...) function.
+  if (s.IsNotFound()) {
+    return DeleteTableRpc(req, resp, rpc);
+  }
+
+  DCHECK(s.ok());
+
+  // The 'reserve_seconds' is specified by the client, means the request coming from a newer
+  // Kudu client with the precise value for 'reserve_seconds', and the field's value from the
+  // request should be taken as-is regardless of the current setting of the
+  // '--default_deleted_table_reserve_seconds' flag at the server side.
+  //
+  // If the 'reserve_seconds' is not specified by the client, means the behavior of DeleteRPC is
+  // controlled by the '--default_deleted_table_reserve_seconds' flag.
+  bool enable_soft_delete_on_client = req.has_reserve_seconds() && req.reserve_seconds() != 0;
+  bool enable_soft_delete_on_master = FLAGS_default_deleted_table_reserve_seconds != 0;
+  bool delete_without_reserving = (req.has_reserve_seconds() && req.reserve_seconds() == 0) ||
+                                (!req.has_reserve_seconds() && !enable_soft_delete_on_master);
+  // Soft-delete a soft-deleted table is not allowed.
+  if (is_soft_deleted_table &&
+      // soft-delete has enabled
+      (enable_soft_delete_on_client || enable_soft_delete_on_master) &&
+      !delete_without_reserving) {
     return SetupError(
-        Status::InvalidArgument(Substitute("soft_deleted table $0 should not be deleted",
+        Status::InvalidArgument(Substitute("soft_deleted table $0 should not be soft deleted again",
                                             req.table().table_name())),
         resp, MasterErrorPB::TABLE_SOFT_DELETED);
   }
 
-  // Reserve seconds equal 0 means delete it directly if default_deleted_table_reserve_seconds
-  // set to 0, which means the cluster-wide behavior of the DeleteTable() RPC is keep default,
-  // or the table is in soft-deleted status.
-  if (req.reserve_seconds() == 0 &&
-      (FLAGS_default_deleted_table_reserve_seconds == 0 || is_soft_deleted_table)) {
+  if (delete_without_reserving) {
     return DeleteTableRpc(req, resp, rpc);
   }
 
diff --git a/src/kudu/master/master.proto b/src/kudu/master/master.proto
index d4e9699..d527ec1 100644
--- a/src/kudu/master/master.proto
+++ b/src/kudu/master/master.proto
@@ -586,6 +586,11 @@
   optional bool modify_external_catalogs = 2 [default = true];
 
   // Reserve seconds after the table has been deleted.
+  // If this field is specified by the client, means the request coming from a newer Kudu
+  // client with the precise value for 'reserve_seconds', and the field's value from the request
+  // should be taken as-is regardless of the current setting of the '--default_deleted_table_reserve_seconds'
+  // flag at the server side.
+  // Otherwise, the behavior of DeleteRPC is controlled by the '--default_deleted_table_reserve_seconds' flag.
   optional uint32 reserve_seconds = 3;
 }
 
diff --git a/src/kudu/tools/kudu-admin-test.cc b/src/kudu/tools/kudu-admin-test.cc
index fd8750a..0a11686 100644
--- a/src/kudu/tools/kudu-admin-test.cc
+++ b/src/kudu/tools/kudu-admin-test.cc
@@ -1652,8 +1652,7 @@
     "table",
     "delete",
     master_address,
-    kTableId,
-    "-reserve_seconds=0"
+    kTableId
   );
 
   vector<string> tables;
diff --git a/src/kudu/tools/kudu-tool-test.cc b/src/kudu/tools/kudu-tool-test.cc
index 76b3507..d088593 100644
--- a/src/kudu/tools/kudu-tool-test.cc
+++ b/src/kudu/tools/kudu-tool-test.cc
@@ -4576,7 +4576,7 @@
 
   // Delete the table.
   NO_FATALS(RunActionStdoutNone(Substitute(
-      "table delete $0 $1 --nomodify_external_catalogs -reserve_seconds=0",
+      "table delete $0 $1 --nomodify_external_catalogs",
       master_addr, kTableName)));
 
   // Check that the table does not exist.
@@ -4633,7 +4633,8 @@
   // Soft-delete the table.
   string out;
   NO_FATALS(RunActionStdoutNone(Substitute(
-      "table delete $0 $1 --reserve_seconds=300", master_addr, kTableName)));
+      "table delete $0 $1 --reserve_seconds=300",
+      master_addr, kTableName)));
 
   // List soft_deleted table.
   vector<string> kudu_tables;
diff --git a/src/kudu/tools/tool_action_table.cc b/src/kudu/tools/tool_action_table.cc
index 56608c9..fa554d4 100644
--- a/src/kudu/tools/tool_action_table.cc
+++ b/src/kudu/tools/tool_action_table.cc
@@ -93,6 +93,7 @@
 using std::find_if;
 using std::make_unique;
 using std::map;
+using std::nullopt;
 using std::ostringstream;
 using std::pair;
 using std::set;
@@ -198,9 +199,9 @@
             "Display the table schema in avro format. When enabled it only outputs the "
             "table schema in Avro format without any other information like "
             "partition/owner/comments. It cannot be used in conjunction with other flags");
-DEFINE_uint32(reserve_seconds, 0,
+DEFINE_int32(reserve_seconds, -1,
               "Grace period before purging a soft-deleted table, in seconds. "
-              "If set to 0, table is purged once it dropped/deleted.");
+              "If set to 0, it would be purged when a table is dropped/deleted.");
 
 DECLARE_bool(create_table);
 DECLARE_bool(fault_tolerant);
@@ -579,7 +580,7 @@
   const KuduSchema& schema = table->schema();
   if (FLAGS_show_avro_format_schema) {
     return PopulateAvroSchema(FindOrDie(context.required_args, kTableNameArg),
-                                         client->cluster_id(), schema);
+                                        client->cluster_id(), schema);
   }
   cout << "TABLE " << table_name << " " << schema.ToString() << endl;
   // The partition schema with current range partitions.