mengw15 commented on code in PR #7744:
URL: https://github.com/apache/texera/pull/7744#discussion_r3810997070


##########
amber/src/test/scala/org/apache/texera/web/service/LakekeeperClientSpec.scala:
##########
@@ -65,13 +65,47 @@ class LakekeeperClientSpec
     exchange.close()
   }
 
+  // Lakekeeper purges dropped tables asynchronously (queue `tabular_purge`), 
and
+  // answers a warehouse delete with 409 WarehouseHasUnfinishedTasks while any
+  // purge task is pending (#7742). These stub warehouses model that queue:
+  // `racing` drains after two attempts, `alwaysBusy` never drains, and
+  // `otherConflict` 409s for an unrelated reason (which must NOT be retried).
+  private val racingWarehouseId = UUID.randomUUID()
+  private val alwaysBusyWarehouseId = UUID.randomUUID()
+  private val otherConflictWarehouseId = UUID.randomUUID()
+  private val malformedConflictWarehouseId = UUID.randomUUID()
+  @volatile private var malformedDeleteAttempts = 0
+  @volatile private var racingDeleteAttempts = 0
+  @volatile private var busyDeleteAttempts = 0

Review Comment:
   Done — all three are gone. The `racing` stub reads the request log too, so 
the per-test reset went with them.



##########
amber/src/main/scala/org/apache/texera/web/service/LakekeeperClient.scala:
##########
@@ -126,12 +144,41 @@ class LakekeeperClient(catalogUri: String = 
StorageConfig.icebergRESTCatalogUri)
         failOn(response.getStatus, response.getBody, s"drop namespace 
'$namespace'")
       }
     }
-    val response = 
Unirest.delete(s"$managementBase/warehouse/$warehouseId").asString()
-    if (response.getStatus != 404) {
-      failOn(response.getStatus, response.getBody, "delete warehouse")
+    // The drops above purge each table's data files asynchronously 
(Lakekeeper task
+    // queue `tabular_purge`), and Lakekeeper refuses to delete the warehouse 
while
+    // any purge is pending — the tasks need the warehouse's storage profile 
to reach
+    // S3, so deleting it first would orphan them and leak the files. It 
answers 409
+    // WarehouseHasUnfinishedTasks until the queue drains (normally within 
seconds),
+    // so ride that out with a bounded retry; every other error, including any 
other
+    // 409, still fails immediately. (#7742)
+    var attempt = 0
+    var delay = unfinishedTasksInitialDelayMillis
+    var deleted = false
+    while (!deleted) {
+      val response = 
Unirest.delete(s"$managementBase/warehouse/$warehouseId").asString()
+      attempt += 1
+      if (response.getStatus == 404 || (response.getStatus >= 200 && 
response.getStatus < 300)) {
+        deleted = true
+      } else if (
+        isUnfinishedTasksConflict(response.getStatus, response.getBody) &&
+        attempt <= unfinishedTasksRetries

Review Comment:
   Good catch — swapped, the budget check short-circuits first now.



##########
amber/src/test/scala/org/apache/texera/web/service/LakekeeperClientSpec.scala:
##########
@@ -65,13 +65,47 @@ class LakekeeperClientSpec
     exchange.close()
   }
 
+  // Lakekeeper purges dropped tables asynchronously (queue `tabular_purge`), 
and
+  // answers a warehouse delete with 409 WarehouseHasUnfinishedTasks while any
+  // purge task is pending (#7742). These stub warehouses model that queue:
+  // `racing` drains after two attempts, `alwaysBusy` never drains, and
+  // `otherConflict` 409s for an unrelated reason (which must NOT be retried).
+  private val racingWarehouseId = UUID.randomUUID()
+  private val alwaysBusyWarehouseId = UUID.randomUUID()
+  private val otherConflictWarehouseId = UUID.randomUUID()
+  private val malformedConflictWarehouseId = UUID.randomUUID()
+  @volatile private var malformedDeleteAttempts = 0
+  @volatile private var racingDeleteAttempts = 0
+  @volatile private var busyDeleteAttempts = 0
+  private val unfinishedTasksBody =
+    """{"error":{"message":"Warehouse has unfinished tasks. Cannot delete 
warehouse until all tasks are 
finished.","type":"WarehouseHasUnfinishedTasks","code":409}}"""
+
   server.createContext(
     "/management/v1/warehouse",
     (exchange: HttpExchange) => {
       record(exchange)
+      val path = exchange.getRequestURI.getPath
+      val isDelete = exchange.getRequestMethod == "DELETE"
       if (exchange.getRequestMethod == "POST") {
         lastCreateBody = new String(exchange.getRequestBody.readAllBytes(), 
StandardCharsets.UTF_8)
         respond(exchange, 201, s"""{"warehouse-id": "$warehouseId"}""")
+      } else if (isDelete && path.endsWith(racingWarehouseId.toString)) {

Review Comment:
   Done — dispatches on the method first, then the path.



##########
amber/src/main/scala/org/apache/texera/web/service/LakekeeperClient.scala:
##########
@@ -39,8 +39,26 @@ import scala.jdk.CollectionConverters.IteratorHasAsScala
   *
   * @param catalogUri the Iceberg REST catalog uri (ends with `/catalog`), 
from which the
   *                   management base is derived. Overridable for tests.
+  * @param unfinishedTasksRetries how many times the final warehouse delete is 
retried while
+  *                               Lakekeeper reports 409 
WarehouseHasUnfinishedTasks — its
+  *                               asynchronous purge of the dropped tables' 
data files is
+  *                               still draining (#7742).
+  * @param unfinishedTasksInitialDelayMillis first pause between those 
retries; it doubles up
+  *                                          to the cap. Starting small keeps 
a fast purge
+  *                                          (the common case) from costing 
the caller a full
+  *                                          fixed interval, while the growth 
keeps a slow one
+  *                                          from hammering Lakekeeper. 
Overridable for tests
+  *                                          (0 keeps the spec free of real 
sleeps — doubling
+  *                                          0 stays 0).
+  * @param unfinishedTasksMaxDelayMillis cap for that doubling. With the 
defaults the retries
+  *                                      wait 0.2+0.4+0.8+1.6+3.2+5+5s ≈ 16s 
in total.
   */
-class LakekeeperClient(catalogUri: String = 
StorageConfig.icebergRESTCatalogUri) {
+class LakekeeperClient(
+    catalogUri: String = StorageConfig.icebergRESTCatalogUri,
+    unfinishedTasksRetries: Int = 7,
+    unfinishedTasksInitialDelayMillis: Long = 200,
+    unfinishedTasksMaxDelayMillis: Long = 5000

Review Comment:
   Done — `LakekeeperClient.PurgeWaitPolicy`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to