From 72682590014c47cf12f894ee54dde8c4601e7532 Mon Sep 17 00:00:00 2001 From: William Hyun Date: Mon, 30 Jun 2025 13:26:10 -0700 Subject: [PATCH 1/4] Create Delegation Service Module --- delegation-service/README.md | 38 +++++++++++++++++++++++++++++ delegation-service/build.gradle.kts | 24 ++++++++++++++++++ gradle/projects.main.properties | 1 + 3 files changed, 63 insertions(+) create mode 100644 delegation-service/README.md create mode 100644 delegation-service/build.gradle.kts diff --git a/delegation-service/README.md b/delegation-service/README.md new file mode 100644 index 0000000000..bf4724b7d7 --- /dev/null +++ b/delegation-service/README.md @@ -0,0 +1,38 @@ +**Under Development** - This module is currently being developed as part of the Polaris Delegation Service. + +# Polaris Delegation Service + +An optional, independent service designed to handle long-running background operations offloaded from the main Polaris catalog service. This service enables the Polaris to maintain low-latency performance for metadata operations while allowing heavy background tasks to be managed and scaled independently. + +## Overview + +The Delegation Service is responsible for executing resource-intensive tasks that would otherwise impact the core performance of the Polaris Catalog. The initial implementation focuses on handling data file deletion processes for `DROP TABLE ... PURGE` commands. + +## Key Features + +- **Independent Scaling**: Can be scaled separately from the main Polaris Catalog workload +- **Resilient Task Execution**: Provides recovery capabilities and retry mechanisms +- **Secure Communication**: Establishes secure communication via mutual TLS with Polaris +- **One-to-One Model**: Each Polaris realm pairs with a dedicated Delegation Service instance + +## Architecture + +The Delegation Service operates as a standalone microservice that: + +1. Receives task delegation requests from Polaris Catalog via REST API +2. Executes long-running operations +3. Maintains its own persistence layer for task tracking +4. Provides synchronous execution for the MVP with plans for async operations + +## Initial Implementation + +The MVP focuses on: +- **DROP TABLE WITH PURGE**: Synchronous delegation of data file deletion +- **Task Persistence**: Independent database schema for task state management + +## Future Roadmap + +- Asynchronous task submission and status tracking +- Integration with Polaris Asynchronous & Reliable Tasks Framework +- Additional delegated operations (compaction, snapshot garbage collection) +- Support for scheduled background tasks diff --git a/delegation-service/build.gradle.kts b/delegation-service/build.gradle.kts new file mode 100644 index 0000000000..1bc15d7503 --- /dev/null +++ b/delegation-service/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.gradle.api.tasks.compile.JavaCompile + +plugins { id("polaris-java") } + +tasks.withType(JavaCompile::class.java).configureEach { options.release = 21 } diff --git a/gradle/projects.main.properties b/gradle/projects.main.properties index fecea71f8c..2457eef3e7 100644 --- a/gradle/projects.main.properties +++ b/gradle/projects.main.properties @@ -20,6 +20,7 @@ polaris-bom=bom polaris-core=polaris-core +polaris-delegation-service=delegation-service polaris-api-iceberg-service=api/iceberg-service polaris-api-management-model=api/management-model polaris-api-management-service=api/management-service From 804995268bc9c84b38f56d167ada47c859129f7c Mon Sep 17 00:00:00 2001 From: William Hyun Date: Mon, 30 Jun 2025 13:56:28 -0700 Subject: [PATCH 2/4] License Header --- delegation-service/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/delegation-service/README.md b/delegation-service/README.md index bf4724b7d7..9a104b106b 100644 --- a/delegation-service/README.md +++ b/delegation-service/README.md @@ -1,3 +1,22 @@ + + **Under Development** - This module is currently being developed as part of the Polaris Delegation Service. # Polaris Delegation Service From dc49bece0caecda202d78b1e57b243cd29a741f1 Mon Sep 17 00:00:00 2001 From: William Hyun Date: Mon, 30 Jun 2025 17:20:13 -0700 Subject: [PATCH 3/4] Add Delegation Service REST Framework for Task Execution --- delegation-service/build.gradle.kts | 29 ++++++ .../polaris/delegation/api/DelegationApi.java | 94 +++++++++++++++++++ .../delegation/api/model/CommonPayload.java | 93 ++++++++++++++++++ .../api/model/OperationParameters.java | 80 ++++++++++++++++ .../delegation/api/model/TableIdentity.java | 93 ++++++++++++++++++ .../api/model/TaskExecutionRequest.java | 79 ++++++++++++++++ .../api/model/TaskExecutionResponse.java | 81 ++++++++++++++++ .../delegation/api/model/TaskType.java | 63 +++++++++++++ .../delegation/service/TaskExecutor.java | 55 +++++++++++ .../service/impl/TaskExecutorImpl.java | 54 +++++++++++ 10 files changed, 721 insertions(+) create mode 100644 delegation-service/src/main/java/org/apache/polaris/delegation/api/DelegationApi.java create mode 100644 delegation-service/src/main/java/org/apache/polaris/delegation/api/model/CommonPayload.java create mode 100644 delegation-service/src/main/java/org/apache/polaris/delegation/api/model/OperationParameters.java create mode 100644 delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TableIdentity.java create mode 100644 delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TaskExecutionRequest.java create mode 100644 delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TaskExecutionResponse.java create mode 100644 delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TaskType.java create mode 100644 delegation-service/src/main/java/org/apache/polaris/delegation/service/TaskExecutor.java create mode 100644 delegation-service/src/main/java/org/apache/polaris/delegation/service/impl/TaskExecutorImpl.java diff --git a/delegation-service/build.gradle.kts b/delegation-service/build.gradle.kts index 1bc15d7503..0cc0093574 100644 --- a/delegation-service/build.gradle.kts +++ b/delegation-service/build.gradle.kts @@ -22,3 +22,32 @@ import org.gradle.api.tasks.compile.JavaCompile plugins { id("polaris-java") } tasks.withType(JavaCompile::class.java).configureEach { options.release = 21 } + +dependencies { + // Core dependencies + implementation(project(":polaris-core")) + + // JAX-RS and REST APIs + implementation(libs.jakarta.ws.rs.api) + implementation(libs.jakarta.inject.api) + implementation(libs.jakarta.validation.api) + implementation(libs.jakarta.enterprise.cdi.api) + + // JSON processing + implementation(platform(libs.jackson.bom)) + implementation("com.fasterxml.jackson.core:jackson-core") + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("com.fasterxml.jackson.core:jackson-annotations") + + // Logging + implementation(libs.slf4j.api) + + // Testing + testImplementation(platform(libs.junit.bom)) + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation(libs.mockito.core) + testImplementation(libs.assertj.core) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + testImplementation(libs.logback.classic) +} diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/api/DelegationApi.java b/delegation-service/src/main/java/org/apache/polaris/delegation/api/DelegationApi.java new file mode 100644 index 0000000000..24e49bd526 --- /dev/null +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/api/DelegationApi.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.delegation.api; + +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.polaris.delegation.api.model.TaskExecutionRequest; +import org.apache.polaris.delegation.service.TaskExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * REST API for the Polaris Delegation Service. + * + *

Provides endpoints for submitting long-running tasks for synchronous execution. This API + * allows the main Polaris catalog to offload resource-intensive operations to maintain low-latency + * performance. + */ +@Path("/api/v1/tasks/execute") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class DelegationApi { + + private static final Logger LOGGER = LoggerFactory.getLogger(DelegationApi.class); + + private final TaskExecutor taskExecutor; + + @Inject + public DelegationApi(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + /** + * Submit a task for delegated execution. + * + *

The task will be executed synchronously and the response will contain the execution result. + * + * @param request the task execution request + * @return the task execution response with completion information + */ + @POST + @Path("/synchronous") + public Response submitTask(@Valid @NotNull TaskExecutionRequest request) { + String operationType = request.getCommonPayload().getOperationType(); + String realmId = request.getCommonPayload().getRealmIdentifier(); + LOGGER.info("Submitting task: operation_type={}, realm_id={}", operationType, realmId); + + try { + var response = taskExecutor.executeTask(request); + return Response.ok(response).build(); + } catch (Exception e) { + LOGGER.error("Failed to execute task", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse("Task execution failed: " + e.getMessage())) + .build(); + } + } + + /** Simple error response model. */ + public static class ErrorResponse { + private final String message; + + public ErrorResponse(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } +} diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/CommonPayload.java b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/CommonPayload.java new file mode 100644 index 0000000000..a53b4c41eb --- /dev/null +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/CommonPayload.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.delegation.api.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import java.time.OffsetDateTime; +import java.util.Objects; + +/** + * Common payload data included in all delegation tasks. + * + *

Contains global task information that applies to all task types within the delegation service. + */ +public class CommonPayload { + + @NotNull private final String operationType; + + @NotNull private final OffsetDateTime requestTimestampUtc; + + @NotNull private final String realmIdentifier; + + @JsonCreator + public CommonPayload( + @JsonProperty("operation_type") @NotNull String operationType, + @JsonProperty("request_timestamp_utc") @NotNull OffsetDateTime requestTimestampUtc, + @JsonProperty("realm_identifier") @NotNull String realmIdentifier) { + this.operationType = operationType; + this.requestTimestampUtc = requestTimestampUtc; + this.realmIdentifier = realmIdentifier; + } + + @JsonProperty("operation_type") + public String getOperationType() { + return operationType; + } + + @JsonProperty("request_timestamp_utc") + public OffsetDateTime getRequestTimestampUtc() { + return requestTimestampUtc; + } + + @JsonProperty("realm_identifier") + public String getRealmIdentifier() { + return realmIdentifier; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CommonPayload that = (CommonPayload) o; + return Objects.equals(operationType, that.operationType) + && Objects.equals(requestTimestampUtc, that.requestTimestampUtc) + && Objects.equals(realmIdentifier, that.realmIdentifier); + } + + @Override + public int hashCode() { + return Objects.hash(operationType, requestTimestampUtc, realmIdentifier); + } + + @Override + public String toString() { + return "CommonPayload{" + + "operationType='" + + operationType + + '\'' + + ", requestTimestampUtc=" + + requestTimestampUtc + + ", realmIdentifier='" + + realmIdentifier + + '\'' + + '}'; + } +} diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/OperationParameters.java b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/OperationParameters.java new file mode 100644 index 0000000000..f8bb483fda --- /dev/null +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/OperationParameters.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.delegation.api.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import java.util.Map; +import java.util.Objects; + +/** + * Operation-specific parameters for delegation tasks. + * + *

Contains parameters specific to the type of operation being performed, such as table identity + * for purge operations and operational properties. + */ +public class OperationParameters { + + @NotNull private final TableIdentity tableIdentity; + + private final Map properties; + + @JsonCreator + public OperationParameters( + @JsonProperty("table_identity") @NotNull TableIdentity tableIdentity, + @JsonProperty("properties") Map properties) { + this.tableIdentity = tableIdentity; + this.properties = properties; + } + + @JsonProperty("table_identity") + public TableIdentity getTableIdentity() { + return tableIdentity; + } + + @JsonProperty("properties") + public Map getProperties() { + return properties; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OperationParameters that = (OperationParameters) o; + return Objects.equals(tableIdentity, that.tableIdentity) + && Objects.equals(properties, that.properties); + } + + @Override + public int hashCode() { + return Objects.hash(tableIdentity, properties); + } + + @Override + public String toString() { + return "OperationParameters{" + + "tableIdentity=" + + tableIdentity + + ", properties=" + + properties + + '}'; + } +} diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TableIdentity.java b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TableIdentity.java new file mode 100644 index 0000000000..51486a1be2 --- /dev/null +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TableIdentity.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.delegation.api.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Objects; + +/** + * Table identity information for delegation tasks. + * + *

Provides the complete hierarchical path to identify a table within the Polaris catalog system. + */ +public class TableIdentity { + + @NotNull private final String catalogName; + + @NotNull private final List namespaceLevels; + + @NotNull private final String tableName; + + @JsonCreator + public TableIdentity( + @JsonProperty("catalog_name") @NotNull String catalogName, + @JsonProperty("namespace_levels") @NotNull List namespaceLevels, + @JsonProperty("table_name") @NotNull String tableName) { + this.catalogName = catalogName; + this.namespaceLevels = namespaceLevels; + this.tableName = tableName; + } + + @JsonProperty("catalog_name") + public String getCatalogName() { + return catalogName; + } + + @JsonProperty("namespace_levels") + public List getNamespaceLevels() { + return namespaceLevels; + } + + @JsonProperty("table_name") + public String getTableName() { + return tableName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TableIdentity that = (TableIdentity) o; + return Objects.equals(catalogName, that.catalogName) + && Objects.equals(namespaceLevels, that.namespaceLevels) + && Objects.equals(tableName, that.tableName); + } + + @Override + public int hashCode() { + return Objects.hash(catalogName, namespaceLevels, tableName); + } + + @Override + public String toString() { + return "TableIdentity{" + + "catalogName='" + + catalogName + + '\'' + + ", namespaceLevels=" + + namespaceLevels + + ", tableName='" + + tableName + + '\'' + + '}'; + } +} diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TaskExecutionRequest.java b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TaskExecutionRequest.java new file mode 100644 index 0000000000..1e2e27a4d3 --- /dev/null +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TaskExecutionRequest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.delegation.api.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import java.util.Objects; + +/** + * Request to execute a delegated task. + * + *

Contains all the information needed to submit a task to the delegation service, structured + * according to the delegation service task payload schema. + */ +public class TaskExecutionRequest { + + @NotNull private final CommonPayload commonPayload; + + @NotNull private final OperationParameters operationParameters; + + @JsonCreator + public TaskExecutionRequest( + @JsonProperty("common_payload") @NotNull CommonPayload commonPayload, + @JsonProperty("operation_parameters") @NotNull OperationParameters operationParameters) { + this.commonPayload = commonPayload; + this.operationParameters = operationParameters; + } + + @JsonProperty("common_payload") + public CommonPayload getCommonPayload() { + return commonPayload; + } + + @JsonProperty("operation_parameters") + public OperationParameters getOperationParameters() { + return operationParameters; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TaskExecutionRequest that = (TaskExecutionRequest) o; + return Objects.equals(commonPayload, that.commonPayload) + && Objects.equals(operationParameters, that.operationParameters); + } + + @Override + public int hashCode() { + return Objects.hash(commonPayload, operationParameters); + } + + @Override + public String toString() { + return "TaskExecutionRequest{" + + "commonPayload=" + + commonPayload + + ", operationParameters=" + + operationParameters + + '}'; + } +} diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TaskExecutionResponse.java b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TaskExecutionResponse.java new file mode 100644 index 0000000000..0726200a97 --- /dev/null +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TaskExecutionResponse.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.delegation.api.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import java.util.Objects; + +/** + * Response from executing a delegated task. + * + *

Contains the execution result and timing information for the completed task. Since the + * delegation service executes tasks synchronously in the MVP, this response indicates the task has + * already been completed. + */ +public class TaskExecutionResponse { + + @NotNull private final String status; + + private final String resultSummary; + + @JsonCreator + public TaskExecutionResponse( + @JsonProperty("status") @NotNull String status, + @JsonProperty("result_summary") String resultSummary) { + this.status = status; + this.resultSummary = resultSummary; + } + + @JsonProperty("status") + public String getStatus() { + return status; + } + + @JsonProperty("result_summary") + public String getResultSummary() { + return resultSummary; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TaskExecutionResponse that = (TaskExecutionResponse) o; + return Objects.equals(status, that.status) && Objects.equals(resultSummary, that.resultSummary); + } + + @Override + public int hashCode() { + return Objects.hash(status, resultSummary); + } + + @Override + public String toString() { + return "TaskExecutionResponse{" + + "status='" + + status + + '\'' + + ", resultSummary='" + + resultSummary + + '\'' + + '}'; + } +} diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TaskType.java b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TaskType.java new file mode 100644 index 0000000000..7b220be938 --- /dev/null +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TaskType.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.delegation.api.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Types of tasks that can be delegated to the Delegation Service. + * + *

This enum defines the various long-running, resource-intensive operations that can be + * offloaded from the main Polaris catalog service. + */ +public enum TaskType { + + /** + * Data file deletion task for DROP TABLE WITH PURGE operations. This is the initial task type + * supported by the delegation service. + */ + PURGE_TABLE("PURGE_TABLE"); + + private final String value; + + TaskType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static TaskType fromValue(String value) { + for (TaskType taskType : TaskType.values()) { + if (taskType.value.equals(value)) { + return taskType; + } + } + throw new IllegalArgumentException("Unknown TaskType: " + value); + } + + @Override + public String toString() { + return value; + } +} diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/service/TaskExecutor.java b/delegation-service/src/main/java/org/apache/polaris/delegation/service/TaskExecutor.java new file mode 100644 index 0000000000..f19d2e645f --- /dev/null +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/service/TaskExecutor.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.delegation.service; + +import org.apache.polaris.delegation.api.model.TaskExecutionRequest; +import org.apache.polaris.delegation.api.model.TaskExecutionResponse; + +/** + * Service interface for executing delegated tasks within the Delegation Service. + * + *

This interface defines methods for executing tasks that have been delegated from the main + * Polaris catalog service. It handles the actual execution of resource-intensive operations to + * maintain low-latency performance for metadata operations in the catalog. + */ +public interface TaskExecutor { + + /** + * Execute a delegated task. + * + *

The task will be executed synchronously and this method will block until the task completes + * successfully or fails with an exception. + * + * @param request the task execution request containing task type, data, and metadata + * @return the task execution response with task ID and completion information + * @throws TaskExecutionException if the task execution fails + */ + TaskExecutionResponse executeTask(TaskExecutionRequest request) throws TaskExecutionException; + + /** Exception thrown when task execution fails. */ + class TaskExecutionException extends Exception { + public TaskExecutionException(String message) { + super(message); + } + + public TaskExecutionException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/service/impl/TaskExecutorImpl.java b/delegation-service/src/main/java/org/apache/polaris/delegation/service/impl/TaskExecutorImpl.java new file mode 100644 index 0000000000..4550e52925 --- /dev/null +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/service/impl/TaskExecutorImpl.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.delegation.service.impl; + +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.polaris.delegation.api.model.TaskExecutionRequest; +import org.apache.polaris.delegation.api.model.TaskExecutionResponse; +import org.apache.polaris.delegation.service.TaskExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Basic implementation of the TaskExecutor for API validation. + * + *

This minimal implementation validates the API contracts and provides a foundation for future + * development. Tasks are acknowledged and return success responses. + */ +@ApplicationScoped +public class TaskExecutorImpl implements TaskExecutor { + + private static final Logger LOGGER = LoggerFactory.getLogger(TaskExecutorImpl.class); + + @Override + public TaskExecutionResponse executeTask(TaskExecutionRequest request) + throws TaskExecutionException { + + String operationType = request.getCommonPayload().getOperationType(); + LOGGER.info("REST API called - Task received: operation_type={}", operationType); + + // TODO: Actual task execution logic will be implemented here + // For now, this is just demonstrating the REST server framework + + LOGGER.info("Returning placeholder response for REST API framework"); + + // Return placeholder response to demonstrate API contract + return new TaskExecutionResponse("COMPLETED", "Task executed successfully"); + } +} From 0e3233fdeeb2a5a7f97012c4ccaccb216a63f874 Mon Sep 17 00:00:00 2001 From: William Hyun Date: Tue, 1 Jul 2025 10:23:28 -0700 Subject: [PATCH 4/4] Restrict PR to just API contracts --- .../polaris/delegation/api/DelegationApi.java | 33 ++--- .../delegation/api/model/CommonPayload.java | 22 ++-- .../api/model/OperationParameters.java | 92 ++++++-------- .../api/model/TablePurgeParameters.java | 119 ++++++++++++++++++ .../delegation/service/TaskExecutor.java | 55 -------- .../service/impl/TaskExecutorImpl.java | 54 -------- 6 files changed, 186 insertions(+), 189 deletions(-) create mode 100644 delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TablePurgeParameters.java delete mode 100644 delegation-service/src/main/java/org/apache/polaris/delegation/service/TaskExecutor.java delete mode 100644 delegation-service/src/main/java/org/apache/polaris/delegation/service/impl/TaskExecutorImpl.java diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/api/DelegationApi.java b/delegation-service/src/main/java/org/apache/polaris/delegation/api/DelegationApi.java index 24e49bd526..e32418cc6b 100644 --- a/delegation-service/src/main/java/org/apache/polaris/delegation/api/DelegationApi.java +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/api/DelegationApi.java @@ -18,7 +18,6 @@ */ package org.apache.polaris.delegation.api; -import jakarta.inject.Inject; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.Consumes; @@ -28,7 +27,8 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.apache.polaris.delegation.api.model.TaskExecutionRequest; -import org.apache.polaris.delegation.service.TaskExecutor; +import org.apache.polaris.delegation.api.model.TaskExecutionResponse; +import org.apache.polaris.delegation.api.model.TaskType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,6 +38,9 @@ *

Provides endpoints for submitting long-running tasks for synchronous execution. This API * allows the main Polaris catalog to offload resource-intensive operations to maintain low-latency * performance. + * + *

Note: This is the initial API framework implementation. The actual task + * execution logic will be implemented in future development phases. */ @Path("/api/v1/tasks/execute") @Consumes(MediaType.APPLICATION_JSON) @@ -46,35 +49,37 @@ public class DelegationApi { private static final Logger LOGGER = LoggerFactory.getLogger(DelegationApi.class); - private final TaskExecutor taskExecutor; - - @Inject - public DelegationApi(TaskExecutor taskExecutor) { - this.taskExecutor = taskExecutor; - } - /** * Submit a task for delegated execution. * *

The task will be executed synchronously and the response will contain the execution result. * + *

Current Implementation: Returns a placeholder response to demonstrate the + * API contract. Actual task execution logic will be implemented in future development phases. + * * @param request the task execution request * @return the task execution response with completion information */ @POST @Path("/synchronous") public Response submitTask(@Valid @NotNull TaskExecutionRequest request) { - String operationType = request.getCommonPayload().getOperationType(); + TaskType taskType = request.getCommonPayload().getTaskType(); String realmId = request.getCommonPayload().getRealmIdentifier(); - LOGGER.info("Submitting task: operation_type={}, realm_id={}", operationType, realmId); + LOGGER.info("Delegation API called - task_type={}, realm_id={}", taskType, realmId); try { - var response = taskExecutor.executeTask(request); + // Placeholder response for API framework demonstration + // TODO: Implement actual task execution logic in future phases + TaskExecutionResponse response = + new TaskExecutionResponse("COMPLETED", "API framework operational - task acknowledged"); + + LOGGER.info("Returning placeholder response for task_type={}", taskType); return Response.ok(response).build(); + } catch (Exception e) { - LOGGER.error("Failed to execute task", e); + LOGGER.error("Failed to process task request", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(new ErrorResponse("Task execution failed: " + e.getMessage())) + .entity(new ErrorResponse("Task processing failed: " + e.getMessage())) .build(); } } diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/CommonPayload.java b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/CommonPayload.java index a53b4c41eb..2172437a5a 100644 --- a/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/CommonPayload.java +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/CommonPayload.java @@ -28,10 +28,11 @@ * Common payload data included in all delegation tasks. * *

Contains global task information that applies to all task types within the delegation service. + * Uses the {@link TaskType} enum to ensure type safety for operation types. */ public class CommonPayload { - @NotNull private final String operationType; + @NotNull private final TaskType taskType; @NotNull private final OffsetDateTime requestTimestampUtc; @@ -39,17 +40,17 @@ public class CommonPayload { @JsonCreator public CommonPayload( - @JsonProperty("operation_type") @NotNull String operationType, + @JsonProperty("task_type") @NotNull TaskType taskType, @JsonProperty("request_timestamp_utc") @NotNull OffsetDateTime requestTimestampUtc, @JsonProperty("realm_identifier") @NotNull String realmIdentifier) { - this.operationType = operationType; + this.taskType = taskType; this.requestTimestampUtc = requestTimestampUtc; this.realmIdentifier = realmIdentifier; } - @JsonProperty("operation_type") - public String getOperationType() { - return operationType; + @JsonProperty("task_type") + public TaskType getTaskType() { + return taskType; } @JsonProperty("request_timestamp_utc") @@ -67,22 +68,21 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CommonPayload that = (CommonPayload) o; - return Objects.equals(operationType, that.operationType) + return Objects.equals(taskType, that.taskType) && Objects.equals(requestTimestampUtc, that.requestTimestampUtc) && Objects.equals(realmIdentifier, that.realmIdentifier); } @Override public int hashCode() { - return Objects.hash(operationType, requestTimestampUtc, realmIdentifier); + return Objects.hash(taskType, requestTimestampUtc, realmIdentifier); } @Override public String toString() { return "CommonPayload{" - + "operationType='" - + operationType - + '\'' + + "taskType=" + + taskType + ", requestTimestampUtc=" + requestTimestampUtc + ", realmIdentifier='" diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/OperationParameters.java b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/OperationParameters.java index f8bb483fda..08900b385c 100644 --- a/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/OperationParameters.java +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/OperationParameters.java @@ -18,63 +18,45 @@ */ package org.apache.polaris.delegation.api.model; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotNull; -import java.util.Map; -import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; /** - * Operation-specific parameters for delegation tasks. + * Base class for operation-specific parameters in delegation tasks. * - *

Contains parameters specific to the type of operation being performed, such as table identity - * for purge operations and operational properties. + *

This abstract class serves as the base for all operation-specific parameter types. Each + * concrete subclass represents parameters for a specific type of delegation operation, providing + * type safety and clear separation of concerns. + * + *

Uses Jackson polymorphism to serialize/deserialize different parameter types based on the + * operation type. This allows the API to handle different parameter structures while maintaining + * type safety. + * + *

Supported Operation Types: + * + *

    + *
  • {@link TablePurgeParameters} - For TABLE_PURGE operations + *
+ * + *

Adding New Operation Types: + * + *

    + *
  1. Create a new concrete subclass extending {@code OperationParameters} + *
  2. Add the subclass to the {@link JsonSubTypes} annotation + *
  3. Define the operation-specific fields and validation + *
*/ -public class OperationParameters { - - @NotNull private final TableIdentity tableIdentity; - - private final Map properties; - - @JsonCreator - public OperationParameters( - @JsonProperty("table_identity") @NotNull TableIdentity tableIdentity, - @JsonProperty("properties") Map properties) { - this.tableIdentity = tableIdentity; - this.properties = properties; - } - - @JsonProperty("table_identity") - public TableIdentity getTableIdentity() { - return tableIdentity; - } - - @JsonProperty("properties") - public Map getProperties() { - return properties; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - OperationParameters that = (OperationParameters) o; - return Objects.equals(tableIdentity, that.tableIdentity) - && Objects.equals(properties, that.properties); - } - - @Override - public int hashCode() { - return Objects.hash(tableIdentity, properties); - } - - @Override - public String toString() { - return "OperationParameters{" - + "tableIdentity=" - + tableIdentity - + ", properties=" - + properties - + '}'; - } +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "task_type") +@JsonSubTypes({@JsonSubTypes.Type(value = TablePurgeParameters.class, name = "PURGE_TABLE")}) +public abstract class OperationParameters { + + /** + * Gets the task type that these parameters support. + * + * @return the task type + */ + public abstract TaskType getTaskType(); } diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TablePurgeParameters.java b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TablePurgeParameters.java new file mode 100644 index 0000000000..a8729e92b3 --- /dev/null +++ b/delegation-service/src/main/java/org/apache/polaris/delegation/api/model/TablePurgeParameters.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.delegation.api.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import java.util.Map; +import java.util.Objects; + +/** + * Operation parameters for table purge operations. + * + *

Contains all the information needed to perform a table purge operation, including the table + * identity and optional configuration properties. + * + *

This class represents parameters for {@code PURGE_TABLE} operations, which handle the deletion + * of data files associated with dropped tables. + * + *

Example Usage: + * + *

+ * TablePurgeParameters params = new TablePurgeParameters(
+ *     new TableIdentity("catalog", List.of("namespace"), "table"),
+ *     Map.of("skipTrash", "true", "batchSize", "1000")
+ * );
+ * 
+ */ +public class TablePurgeParameters extends OperationParameters { + + @NotNull private final TableIdentity tableIdentity; + + private final Map properties; + + @JsonCreator + public TablePurgeParameters( + @JsonProperty("table_identity") @NotNull TableIdentity tableIdentity, + @JsonProperty("properties") Map properties) { + this.tableIdentity = tableIdentity; + this.properties = properties; + } + + @Override + public TaskType getTaskType() { + return TaskType.PURGE_TABLE; + } + + @JsonProperty("table_identity") + @NotNull + public TableIdentity getTableIdentity() { + return tableIdentity; + } + + @JsonProperty("properties") + public Map getProperties() { + return properties; + } + + /** + * Gets a property value by key. + * + * @param key the property key + * @return the property value, or null if not found + */ + public String getProperty(String key) { + return properties != null ? properties.get(key) : null; + } + + /** + * Checks if a property is set to "true". + * + * @param key the property key + * @return true if the property exists and equals "true" (case-insensitive) + */ + public boolean getBooleanProperty(String key) { + String value = getProperty(key); + return "true".equalsIgnoreCase(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TablePurgeParameters that = (TablePurgeParameters) o; + return Objects.equals(tableIdentity, that.tableIdentity) + && Objects.equals(properties, that.properties); + } + + @Override + public int hashCode() { + return Objects.hash(tableIdentity, properties); + } + + @Override + public String toString() { + return "TablePurgeParameters{" + + "tableIdentity=" + + tableIdentity + + ", properties=" + + properties + + '}'; + } +} diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/service/TaskExecutor.java b/delegation-service/src/main/java/org/apache/polaris/delegation/service/TaskExecutor.java deleted file mode 100644 index f19d2e645f..0000000000 --- a/delegation-service/src/main/java/org/apache/polaris/delegation/service/TaskExecutor.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.polaris.delegation.service; - -import org.apache.polaris.delegation.api.model.TaskExecutionRequest; -import org.apache.polaris.delegation.api.model.TaskExecutionResponse; - -/** - * Service interface for executing delegated tasks within the Delegation Service. - * - *

This interface defines methods for executing tasks that have been delegated from the main - * Polaris catalog service. It handles the actual execution of resource-intensive operations to - * maintain low-latency performance for metadata operations in the catalog. - */ -public interface TaskExecutor { - - /** - * Execute a delegated task. - * - *

The task will be executed synchronously and this method will block until the task completes - * successfully or fails with an exception. - * - * @param request the task execution request containing task type, data, and metadata - * @return the task execution response with task ID and completion information - * @throws TaskExecutionException if the task execution fails - */ - TaskExecutionResponse executeTask(TaskExecutionRequest request) throws TaskExecutionException; - - /** Exception thrown when task execution fails. */ - class TaskExecutionException extends Exception { - public TaskExecutionException(String message) { - super(message); - } - - public TaskExecutionException(String message, Throwable cause) { - super(message, cause); - } - } -} diff --git a/delegation-service/src/main/java/org/apache/polaris/delegation/service/impl/TaskExecutorImpl.java b/delegation-service/src/main/java/org/apache/polaris/delegation/service/impl/TaskExecutorImpl.java deleted file mode 100644 index 4550e52925..0000000000 --- a/delegation-service/src/main/java/org/apache/polaris/delegation/service/impl/TaskExecutorImpl.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.polaris.delegation.service.impl; - -import jakarta.enterprise.context.ApplicationScoped; -import org.apache.polaris.delegation.api.model.TaskExecutionRequest; -import org.apache.polaris.delegation.api.model.TaskExecutionResponse; -import org.apache.polaris.delegation.service.TaskExecutor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Basic implementation of the TaskExecutor for API validation. - * - *

This minimal implementation validates the API contracts and provides a foundation for future - * development. Tasks are acknowledged and return success responses. - */ -@ApplicationScoped -public class TaskExecutorImpl implements TaskExecutor { - - private static final Logger LOGGER = LoggerFactory.getLogger(TaskExecutorImpl.class); - - @Override - public TaskExecutionResponse executeTask(TaskExecutionRequest request) - throws TaskExecutionException { - - String operationType = request.getCommonPayload().getOperationType(); - LOGGER.info("REST API called - Task received: operation_type={}", operationType); - - // TODO: Actual task execution logic will be implemented here - // For now, this is just demonstrating the REST server framework - - LOGGER.info("Returning placeholder response for REST API framework"); - - // Return placeholder response to demonstrate API contract - return new TaskExecutionResponse("COMPLETED", "Task executed successfully"); - } -}