This is an automated email from the ASF dual-hosted git repository.

nicholasjiang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-webui.git


The following commit(s) were added to refs/heads/main by this push:
     new 098e6f3  [Feature] Introduce Session Interface (#209)
098e6f3 is described below

commit 098e6f3421556b7f5f6f9236300cdbbc7804a17a
Author: s7monk <[email protected]>
AuthorDate: Mon May 13 11:45:13 2024 +0800

    [Feature] Introduce Session Interface (#209)
---
 paimon-web-server/pom.xml                          |  56 ++++++
 .../paimon/web/server/configrue/RetryConfig.java   |  27 +++
 .../web/server/controller/SessionController.java   |  90 +++++++++
 .../paimon/web/server/data/dto/SessionDTO.java     |  34 ++++
 .../paimon/web/server/service/SessionService.java  |  57 ++++++
 .../web/server/service/UserSessionManager.java     |  50 +++++
 .../server/service/impl/SessionServiceImpl.java    | 105 +++++++++++
 .../server/controller/FlinkSQLGatewayTestBase.java | 201 +++++++++++++++++++++
 .../server/controller/SessionControllerTest.java   | 175 ++++++++++++++++++
 9 files changed, 795 insertions(+)

diff --git a/paimon-web-server/pom.xml b/paimon-web-server/pom.xml
index 5aee2a6..ebd3c7f 100644
--- a/paimon-web-server/pom.xml
+++ b/paimon-web-server/pom.xml
@@ -36,6 +36,7 @@ under the License.
 
     <properties>
         <hadoop.version>2.8.5</hadoop.version>
+        <flink.version>1.18.1</flink.version>
     </properties>
 
     <dependencies>
@@ -51,6 +52,12 @@ under the License.
             <version>${project.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.paimon</groupId>
+            <artifactId>paimon-web-gateway</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>cn.hutool</groupId>
             <artifactId>hutool-all</artifactId>
@@ -154,6 +161,16 @@ under the License.
             <artifactId>mybatis-plus-boot-starter</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.retry</groupId>
+            <artifactId>spring-retry</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-aspects</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>cn.dev33</groupId>
             <artifactId>sa-token-spring-boot-starter</artifactId>
@@ -172,20 +189,59 @@ under the License.
             <artifactId>hadoop-client</artifactId>
             <version>${hadoop.version}</version>
             <scope>provided</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>commons-cli</groupId>
+                    <artifactId>commons-cli</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <dependency>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>
         </dependency>
+
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-actuator</artifactId>
         </dependency>
+
         <dependency>
             <groupId>org.springdoc</groupId>
             <artifactId>springdoc-openapi-ui</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.apache.flink</groupId>
+            <artifactId>flink-test-utils</artifactId>
+            <version>${flink.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <artifactId>log4j-slf4j-impl</artifactId>
+                    <groupId>org.apache.logging.log4j</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>log4j-core</artifactId>
+                    <groupId>org.apache.logging.log4j</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>log4j-api</artifactId>
+                    <groupId>org.apache.logging.log4j</groupId>
+                </exclusion>
+                <exclusion>
+                    <groupId>commons-cli</groupId>
+                    <artifactId>commons-cli</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-cli</groupId>
+            <artifactId>commons-cli</artifactId>
+            <version>1.4</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git 
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/configrue/RetryConfig.java
 
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/configrue/RetryConfig.java
new file mode 100644
index 0000000..82ab4a6
--- /dev/null
+++ 
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/configrue/RetryConfig.java
@@ -0,0 +1,27 @@
+/*
+ * 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.paimon.web.server.configrue;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.retry.annotation.EnableRetry;
+
+/** RetryConfig. */
+@EnableRetry
+@Configuration
+public class RetryConfig {}
diff --git 
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/SessionController.java
 
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/SessionController.java
new file mode 100644
index 0000000..ff2f641
--- /dev/null
+++ 
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/SessionController.java
@@ -0,0 +1,90 @@
+/*
+ * 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.paimon.web.server.controller;
+
+import org.apache.paimon.web.server.data.dto.SessionDTO;
+import org.apache.paimon.web.server.data.model.ClusterInfo;
+import org.apache.paimon.web.server.data.result.R;
+import org.apache.paimon.web.server.service.ClusterService;
+import org.apache.paimon.web.server.service.SessionService;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.retry.annotation.Backoff;
+import org.springframework.retry.annotation.Recover;
+import org.springframework.retry.annotation.Retryable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/** Session api controller. */
+@Slf4j
+@RestController
+@RequestMapping("/api/session")
+public class SessionController {
+
+    @Autowired private SessionService sessionService;
+
+    @Autowired private ClusterService clusterService;
+
+    @PostMapping("/create")
+    @Retryable(
+            value = {Exception.class},
+            maxAttempts = 3,
+            backoff = @Backoff(delay = 3000))
+    public R<Void> createSession(Integer uid) {
+        QueryWrapper<ClusterInfo> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("type", "Flink");
+        List<ClusterInfo> clusterInfos = clusterService.list(queryWrapper);
+        for (ClusterInfo cluster : clusterInfos) {
+            SessionDTO sessionDTO = new SessionDTO();
+            sessionDTO.setHost(cluster.getHost());
+            sessionDTO.setPort(cluster.getPort());
+            sessionDTO.setClusterId(cluster.getId());
+            sessionDTO.setUid(uid);
+            sessionService.createSession(sessionDTO);
+        }
+        return R.succeed();
+    }
+
+    @Recover
+    public R<Void> recover(Exception e, Integer uid) {
+        log.error("After retries failed to create session for UID: {}", uid, 
e);
+        return R.failed();
+    }
+
+    @PostMapping("/drop")
+    public R<Void> dropSession(Integer uid) {
+        QueryWrapper<ClusterInfo> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("type", "Flink");
+        List<ClusterInfo> clusterInfos = clusterService.list(queryWrapper);
+        for (ClusterInfo cluster : clusterInfos) {
+            SessionDTO sessionDTO = new SessionDTO();
+            sessionDTO.setHost(cluster.getHost());
+            sessionDTO.setPort(cluster.getPort());
+            sessionDTO.setClusterId(cluster.getId());
+            sessionDTO.setUid(uid);
+            sessionService.closeSession(sessionDTO);
+        }
+        return R.succeed();
+    }
+}
diff --git 
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/SessionDTO.java
 
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/SessionDTO.java
new file mode 100644
index 0000000..1399c60
--- /dev/null
+++ 
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/SessionDTO.java
@@ -0,0 +1,34 @@
+/*
+ * 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.paimon.web.server.data.dto;
+
+import lombok.Data;
+
+/** DTO of session. */
+@Data
+public class SessionDTO {
+
+    private Integer uid;
+
+    private Integer clusterId;
+
+    private String host;
+
+    private Integer port;
+}
diff --git 
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/SessionService.java
 
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/SessionService.java
new file mode 100644
index 0000000..4204e87
--- /dev/null
+++ 
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/SessionService.java
@@ -0,0 +1,57 @@
+/*
+ * 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.paimon.web.server.service;
+
+import org.apache.paimon.web.engine.flink.sql.gateway.model.SessionEntity;
+import org.apache.paimon.web.server.data.dto.SessionDTO;
+
+/** Session Service. */
+public interface SessionService {
+
+    /**
+     * Creates a new session.
+     *
+     * @param sessionDTO the data transfer object containing session details
+     */
+    void createSession(SessionDTO sessionDTO);
+
+    /**
+     * Closes an existing session.
+     *
+     * @param sessionDTO the data transfer object containing session details
+     */
+    void closeSession(SessionDTO sessionDTO);
+
+    /**
+     * Triggers a heartbeat update for a session.
+     *
+     * @param sessionDTO the data transfer object containing session details
+     * @return the status code after triggering the heartbeat
+     */
+    int triggerSessionHeartbeat(SessionDTO sessionDTO);
+
+    /**
+     * Retrieves the session for a given user ID within a specified cluster.
+     *
+     * @param uid the unique identifier of the user
+     * @param clusterId the identifier of the cluster
+     * @return the SessionEntity for the specified user and cluster, or null 
if no session is found
+     */
+    SessionEntity getSession(Integer uid, Integer clusterId);
+}
diff --git 
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/UserSessionManager.java
 
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/UserSessionManager.java
new file mode 100644
index 0000000..8fd5dc9
--- /dev/null
+++ 
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/UserSessionManager.java
@@ -0,0 +1,50 @@
+/*
+ * 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.paimon.web.server.service;
+
+import org.apache.paimon.web.engine.flink.sql.gateway.model.SessionEntity;
+
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Manages user sessions for Flink SQL Gateway. */
+@Service
+public class UserSessionManager {
+
+    private final ConcurrentHashMap<String, SessionEntity> sessions = new 
ConcurrentHashMap<>();
+
+    public SessionEntity getSession(String id) {
+        return sessions.get(id);
+    }
+
+    public void addSession(String id, SessionEntity session) {
+        sessions.put(id, session);
+    }
+
+    public void removeSession(String id) {
+        sessions.remove(id);
+    }
+
+    public List<SessionEntity> getAllSessions() {
+        return new ArrayList<>(sessions.values());
+    }
+}
diff --git 
a/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/SessionServiceImpl.java
 
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/SessionServiceImpl.java
new file mode 100644
index 0000000..4848f5a
--- /dev/null
+++ 
b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/SessionServiceImpl.java
@@ -0,0 +1,105 @@
+/*
+ * 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.paimon.web.server.service.impl;
+
+import org.apache.paimon.web.engine.flink.sql.gateway.client.SqlGatewayClient;
+import org.apache.paimon.web.engine.flink.sql.gateway.model.SessionEntity;
+import org.apache.paimon.web.server.data.dto.SessionDTO;
+import org.apache.paimon.web.server.service.SessionService;
+import org.apache.paimon.web.server.service.UserService;
+import org.apache.paimon.web.server.service.UserSessionManager;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.UUID;
+
+/** The implementation of {@link SessionService}. */
+@Service
+public class SessionServiceImpl implements SessionService {
+
+    private static final Integer ACTIVE_STATUS = 1;
+    private static final Integer INACTIVE_STATUS = 0;
+
+    @Autowired private UserSessionManager sessionManager;
+
+    @Autowired private UserService userService;
+
+    @Override
+    public void createSession(SessionDTO sessionDTO) {
+        try {
+            SqlGatewayClient client =
+                    new SqlGatewayClient(sessionDTO.getHost(), 
sessionDTO.getPort());
+            if (sessionDTO.getUid() != null) {
+                String username = 
userService.getUserById(sessionDTO.getUid()).getUsername();
+                String sessionName = username + "_" + UUID.randomUUID();
+                if (getSession(sessionDTO.getUid(), sessionDTO.getClusterId()) 
== null
+                        || triggerSessionHeartbeat(sessionDTO) < 1) {
+                    SessionEntity sessionEntity = 
client.openSession(sessionName);
+                    sessionManager.addSession(
+                            sessionDTO.getUid() + "_" + 
sessionDTO.getClusterId(), sessionEntity);
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create session", e);
+        }
+    }
+
+    @Override
+    public void closeSession(SessionDTO sessionDTO) {
+        try {
+            SqlGatewayClient client =
+                    new SqlGatewayClient(sessionDTO.getHost(), 
sessionDTO.getPort());
+            if (sessionDTO.getUid() != null) {
+                SessionEntity session =
+                        sessionManager.getSession(
+                                sessionDTO.getUid() + "_" + 
sessionDTO.getClusterId());
+                if (session != null) {
+                    client.closeSession(session.getSessionId());
+                    sessionManager.removeSession(
+                            sessionDTO.getUid() + "_" + 
sessionDTO.getClusterId());
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to close session", e);
+        }
+    }
+
+    @Override
+    public int triggerSessionHeartbeat(SessionDTO sessionDTO) {
+        try {
+            if (sessionDTO.getUid() != null) {
+                SqlGatewayClient client =
+                        new SqlGatewayClient(sessionDTO.getHost(), 
sessionDTO.getPort());
+                SessionEntity session =
+                        sessionManager.getSession(
+                                sessionDTO.getUid() + "_" + 
sessionDTO.getClusterId());
+                client.triggerSessionHeartbeat(session.getSessionId());
+            }
+        } catch (Exception e) {
+            return INACTIVE_STATUS;
+        }
+        return ACTIVE_STATUS;
+    }
+
+    @Override
+    public SessionEntity getSession(Integer uid, Integer clusterId) {
+        return sessionManager.getSession(uid + "_" + clusterId);
+    }
+}
diff --git 
a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/FlinkSQLGatewayTestBase.java
 
b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/FlinkSQLGatewayTestBase.java
new file mode 100644
index 0000000..59f7312
--- /dev/null
+++ 
b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/FlinkSQLGatewayTestBase.java
@@ -0,0 +1,201 @@
+/*
+ * 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.paimon.web.server.controller;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.core.testutils.CommonTestUtils;
+import org.apache.flink.table.gateway.api.SqlGatewayService;
+import 
org.apache.flink.table.gateway.api.endpoint.SqlGatewayEndpointFactoryUtils;
+import org.apache.flink.table.gateway.rest.SqlGatewayRestEndpoint;
+import org.apache.flink.table.gateway.rest.util.SqlGatewayRestOptions;
+import org.apache.flink.table.gateway.service.SqlGatewayServiceImpl;
+import org.apache.flink.table.gateway.service.context.DefaultContext;
+import org.apache.flink.table.gateway.service.session.SessionManager;
+import org.apache.flink.test.junit5.MiniClusterExtension;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.Extension;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.rules.TemporaryFolder;
+
+import javax.annotation.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import static 
org.apache.flink.configuration.ConfigConstants.ENV_FLINK_CONF_DIR;
+import static 
org.apache.flink.table.gateway.api.endpoint.SqlGatewayEndpointFactoryUtils.getEndpointConfig;
+import static 
org.apache.flink.table.gateway.api.endpoint.SqlGatewayEndpointFactoryUtils.getSqlGatewayOptionPrefix;
+import static 
org.apache.flink.table.gateway.rest.SqlGatewayRestEndpointFactory.IDENTIFIER;
+import static 
org.apache.flink.table.gateway.rest.SqlGatewayRestEndpointFactory.rebuildRestEndpointOptions;
+import static org.apache.flink.util.Preconditions.checkNotNull;
+
+/** The base class for sql gateway test. */
+public class FlinkSQLGatewayTestBase {
+
+    @RegisterExtension
+    @Order(1)
+    private static final MiniClusterExtension MINI_CLUSTER = new 
MiniClusterExtension();
+
+    @RegisterExtension
+    @Order(2)
+    protected static final SqlGatewayServiceExtension 
SQL_GATEWAY_SERVICE_EXTENSION =
+            new 
SqlGatewayServiceExtension(MINI_CLUSTER::getClientConfiguration);
+
+    @Nullable protected static String targetAddress = null;
+    @Nullable private static SqlGatewayRestEndpoint sqlGatewayRestEndpoint = 
null;
+
+    protected static int port = 0;
+
+    @BeforeAll
+    static void start() throws Exception {
+        final String address = 
InetAddress.getLoopbackAddress().getHostAddress();
+        Configuration config = getBaseConfig(getFlinkConfig(address, address, 
"0"));
+        sqlGatewayRestEndpoint =
+                new SqlGatewayRestEndpoint(config, 
SQL_GATEWAY_SERVICE_EXTENSION.getService());
+        sqlGatewayRestEndpoint.start();
+        InetSocketAddress serverAddress = 
checkNotNull(sqlGatewayRestEndpoint.getServerAddress());
+        targetAddress = serverAddress.getHostName();
+        port = serverAddress.getPort();
+    }
+
+    @AfterAll
+    static void stop() throws Exception {
+        checkNotNull(sqlGatewayRestEndpoint);
+        sqlGatewayRestEndpoint.close();
+    }
+
+    private static Configuration getBaseConfig(Configuration flinkConf) {
+        SqlGatewayEndpointFactoryUtils.DefaultEndpointFactoryContext context =
+                new 
SqlGatewayEndpointFactoryUtils.DefaultEndpointFactoryContext(
+                        null, flinkConf, getEndpointConfig(flinkConf, 
IDENTIFIER));
+
+        return rebuildRestEndpointOptions(context.getEndpointOptions());
+    }
+
+    private static Configuration getFlinkConfig(
+            String address, String bindAddress, String portRange) {
+        final Configuration config = new Configuration();
+        if (address != null) {
+            config.setString(
+                    
getSqlGatewayRestOptionFullName(SqlGatewayRestOptions.ADDRESS.key()), address);
+        }
+        if (bindAddress != null) {
+            config.setString(
+                    
getSqlGatewayRestOptionFullName(SqlGatewayRestOptions.BIND_ADDRESS.key()),
+                    bindAddress);
+        }
+        if (portRange != null) {
+            config.setString(
+                    
getSqlGatewayRestOptionFullName(SqlGatewayRestOptions.PORT.key()), portRange);
+        }
+        return config;
+    }
+
+    private static String getSqlGatewayRestOptionFullName(String key) {
+        return getSqlGatewayOptionPrefix(IDENTIFIER) + key;
+    }
+
+    /** A simple {@link Extension} to be used by tests that require a {@link 
SqlGatewayService}. */
+    static class SqlGatewayServiceExtension implements BeforeAllCallback, 
AfterAllCallback {
+
+        private SqlGatewayService service;
+        private SessionManager sessionManager;
+        private TemporaryFolder temporaryFolder;
+        private final Supplier<Configuration> configSupplier;
+        private final Function<DefaultContext, SessionManager> 
sessionManagerCreator;
+
+        public SqlGatewayServiceExtension(Supplier<Configuration> 
configSupplier) {
+            this(configSupplier, SessionManager::create);
+        }
+
+        public SqlGatewayServiceExtension(
+                Supplier<Configuration> configSupplier,
+                Function<DefaultContext, SessionManager> 
sessionManagerCreator) {
+            this.configSupplier = configSupplier;
+            this.sessionManagerCreator = sessionManagerCreator;
+        }
+
+        @Override
+        public void beforeAll(ExtensionContext context) throws Exception {
+            final Map<String, String> originalEnv = System.getenv();
+            try {
+                // prepare conf dir
+                temporaryFolder = new TemporaryFolder();
+                temporaryFolder.create();
+                File confFolder = temporaryFolder.newFolder("conf");
+                File confYaml = new File(confFolder, "flink-conf.yaml");
+                if (!confYaml.createNewFile()) {
+                    throw new IOException("Can't create testing 
flink-conf.yaml file.");
+                }
+
+                FileUtils.write(
+                        confYaml,
+                        getFlinkConfContent(configSupplier.get().toMap()),
+                        StandardCharsets.UTF_8);
+
+                // adjust the test environment for the purposes of this test
+                Map<String, String> map = new HashMap<>(System.getenv());
+                map.put(ENV_FLINK_CONF_DIR, confFolder.getAbsolutePath());
+                CommonTestUtils.setEnv(map);
+
+                sessionManager =
+                        sessionManagerCreator.apply(
+                                DefaultContext.load(
+                                        new Configuration(), 
Collections.emptyList(), true, false));
+            } finally {
+                CommonTestUtils.setEnv(originalEnv);
+            }
+
+            service = new SqlGatewayServiceImpl(sessionManager);
+            sessionManager.start();
+        }
+
+        @Override
+        public void afterAll(ExtensionContext context) throws Exception {
+            if (sessionManager != null) {
+                sessionManager.stop();
+            }
+            temporaryFolder.delete();
+        }
+
+        public SqlGatewayService getService() {
+            return service;
+        }
+
+        private String getFlinkConfContent(Map<String, String> flinkConf) {
+            StringBuilder sb = new StringBuilder();
+            flinkConf.forEach((k, v) -> sb.append(k).append(": 
").append(v).append("\n"));
+            return sb.toString();
+        }
+    }
+}
diff --git 
a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/SessionControllerTest.java
 
b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/SessionControllerTest.java
new file mode 100644
index 0000000..c2bcda4
--- /dev/null
+++ 
b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/SessionControllerTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.paimon.web.server.controller;
+
+import org.apache.paimon.web.engine.flink.sql.gateway.model.SessionEntity;
+import org.apache.paimon.web.server.data.dto.LoginDTO;
+import org.apache.paimon.web.server.data.model.ClusterInfo;
+import org.apache.paimon.web.server.data.result.R;
+import org.apache.paimon.web.server.service.ClusterService;
+import org.apache.paimon.web.server.service.UserSessionManager;
+import org.apache.paimon.web.server.util.ObjectMapperUtils;
+import org.apache.paimon.web.server.util.StringUtils;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import 
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockCookie;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Test for {@link SessionController}. */
+@SpringBootTest
+@AutoConfigureMockMvc
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class SessionControllerTest extends FlinkSQLGatewayTestBase {
+
+    private static final String loginPath = "/api/login";
+    private static final String logoutPath = "/api/logout";
+    private static final String sessionPath = "/api/session";
+
+    @Value("${spring.application.name}")
+    private String tokenName;
+
+    @Autowired public MockMvc mockMvc;
+
+    public static MockCookie cookie;
+
+    @Autowired private ClusterService clusterService;
+
+    @Autowired private UserSessionManager sessionManager;
+
+    @BeforeEach
+    public void before() throws Exception {
+        LoginDTO login = new LoginDTO();
+        login.setUsername("admin");
+        login.setPassword("admin");
+        MockHttpServletResponse response =
+                mockMvc.perform(
+                                MockMvcRequestBuilders.post(loginPath)
+                                        
.content(ObjectMapperUtils.toJSON(login))
+                                        
.contentType(MediaType.APPLICATION_JSON_VALUE)
+                                        
.accept(MediaType.APPLICATION_JSON_VALUE))
+                        .andExpect(MockMvcResultMatchers.status().isOk())
+                        .andDo(MockMvcResultHandlers.print())
+                        .andReturn()
+                        .getResponse();
+        String result = response.getContentAsString();
+        R<?> r = ObjectMapperUtils.fromJSON(result, R.class);
+        assertEquals(200, r.getCode());
+
+        assertTrue(StringUtils.isNotBlank(r.getData().toString()));
+
+        cookie = (MockCookie) response.getCookie(tokenName);
+    }
+
+    @AfterEach
+    public void after() throws Exception {
+        String result =
+                mockMvc.perform(
+                                MockMvcRequestBuilders.post(logoutPath)
+                                        .cookie(cookie)
+                                        
.contentType(MediaType.APPLICATION_JSON_VALUE)
+                                        
.accept(MediaType.APPLICATION_JSON_VALUE))
+                        .andExpect(MockMvcResultMatchers.status().isOk())
+                        .andDo(MockMvcResultHandlers.print())
+                        .andReturn()
+                        .getResponse()
+                        .getContentAsString();
+        R<?> r = ObjectMapperUtils.fromJSON(result, R.class);
+        assertEquals(200, r.getCode());
+    }
+
+    @Test
+    @Order(1)
+    public void testCreateSession() throws Exception {
+        ClusterInfo cluster =
+                ClusterInfo.builder()
+                        .clusterName("test_cluster")
+                        .host(targetAddress)
+                        .port(port)
+                        .enabled(true)
+                        .type("Flink")
+                        .build();
+        boolean res = clusterService.save(cluster);
+        assertTrue(res);
+
+        QueryWrapper<ClusterInfo> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("cluster_name", "test_cluster");
+        ClusterInfo one = clusterService.getOne(queryWrapper);
+        String responseString =
+                mockMvc.perform(
+                                MockMvcRequestBuilders.post(sessionPath + 
"/create")
+                                        .cookie(cookie)
+                                        .param("uid", "1")
+                                        
.contentType(MediaType.APPLICATION_JSON_VALUE)
+                                        
.accept(MediaType.APPLICATION_JSON_VALUE))
+                        .andExpect(MockMvcResultMatchers.status().isOk())
+                        .andDo(MockMvcResultHandlers.print())
+                        .andReturn()
+                        .getResponse()
+                        .getContentAsString();
+        R<Void> r = ObjectMapperUtils.fromJSON(responseString, new 
TypeReference<R<Void>>() {});
+        assertEquals(200, r.getCode());
+        SessionEntity session = sessionManager.getSession("1" + "_" + 
one.getId());
+        assertEquals(session.getHost(), targetAddress);
+        assertEquals(session.getPort(), port);
+    }
+
+    @Test
+    @Order(2)
+    public void testDropSession() throws Exception {
+        QueryWrapper<ClusterInfo> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("cluster_name", "test_cluster");
+        ClusterInfo one = clusterService.getOne(queryWrapper);
+        String responseString =
+                mockMvc.perform(
+                                MockMvcRequestBuilders.post(sessionPath + 
"/drop")
+                                        .cookie(cookie)
+                                        .param("uid", "1")
+                                        
.contentType(MediaType.APPLICATION_JSON_VALUE)
+                                        
.accept(MediaType.APPLICATION_JSON_VALUE))
+                        .andExpect(MockMvcResultMatchers.status().isOk())
+                        .andDo(MockMvcResultHandlers.print())
+                        .andReturn()
+                        .getResponse()
+                        .getContentAsString();
+        R<Void> r = ObjectMapperUtils.fromJSON(responseString, new 
TypeReference<R<Void>>() {});
+        assertEquals(200, r.getCode());
+        SessionEntity session = sessionManager.getSession("1" + "_" + 
one.getId());
+        assertNull(session);
+    }
+}


Reply via email to