From 575eba6b5dc4d4779ca2397ee1e52f5133fa173e Mon Sep 17 00:00:00 2001 From: mgrati Date: Fri, 2 Jan 2026 15:50:39 +0100 Subject: [PATCH 1/3] Fix : Remove database connection closure from the detach listener [APPS-02TI] --- .../visual/ui/vaadin/main/MainWindow.kt | 84 ------------------- .../visual/ui/vaadin/visual/VApplication.kt | 4 - gradle.properties | 2 +- 3 files changed, 1 insertion(+), 89 deletions(-) diff --git a/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/main/MainWindow.kt b/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/main/MainWindow.kt index bfe4b6fec..373ace6d1 100644 --- a/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/main/MainWindow.kt +++ b/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/main/MainWindow.kt @@ -38,7 +38,6 @@ import com.vaadin.flow.component.Key import com.vaadin.flow.component.KeyModifier import com.vaadin.flow.component.ShortcutEvent import com.vaadin.flow.component.Shortcuts -import com.vaadin.flow.component.UI import com.vaadin.flow.component.contextmenu.MenuItem import com.vaadin.flow.component.dependency.CssImport import com.vaadin.flow.component.html.Div @@ -104,7 +103,6 @@ class MainWindow(locale: Locale, val logo: String, val href: String, val applica addLinksListeners() Shortcuts.addShortcutListener(this, this::goToPreviousPage, Key.PAGE_UP, KeyModifier.of("Alt")) Shortcuts.addShortcutListener(this, this::goToNextPage, Key.PAGE_DOWN, KeyModifier.of("Alt")) - addBeforeUnloadListener() instance = this } @@ -243,88 +241,6 @@ class MainWindow(locale: Locale, val logo: String, val href: String, val applica } } - /** - * Adds a detatch listener to be excecuted only when a window is closed or navigated away - */ - fun addWindowDetachListener(onDetach: () -> Unit) { - addDetachListener { - isWindowActuallyClosing { isClosing -> - if (isClosing) { - println("The window is closing or navigating away.") - onDetach() - } else { - println("The window was refreshed.") - } - } - } - } - - /** - * Determines if the window is actually closing (i.e., the user is closing a tab or - * navigating away) as opposed to simply refreshing the page. - * - * This method uses a `localStorage` flag, `pageReload`, that is set or removed by the - * `addBeforeUnloadListener` method. The logic works as follows: - * - * - If `pageReload` is found and set to `'true'`, it indicates that a page refresh - * occurred. In this case, the method returns `false` to indicate that the window - * is not actually closing. - * - If `pageReload` is not found, it suggests that the window is closing (either - * a tab close or navigation away), so the method returns `true`. - * - * @param callback A lambda function to handle the result: `true` if the window is closing (not a refresh), `false` otherwise. - */ - private fun isWindowActuallyClosing(callback: (Boolean) -> Unit) { - UI.getCurrent().page.executeJs("return localStorage.getItem('pageReload') === 'true';").then { - val isRefresh = it.asBoolean() - - callback(!isRefresh) - } - } - - /** - * Adds a JavaScript listener for the `beforeunload` event to detect when a page is - * refreshed or when a tab is being closed/navigated away from. - * - * This listener distinguishes between a page refresh (navigation type `1`) and other - * events such as closing the browser tab or navigating away. It uses `localStorage` - * to set a flag based on the detected action, which can be used server-side to infer - * user actions when combined with a Vaadin `DetachListener`. - * - * - On page refresh: A flag `pageReload` is set in the browser's `localStorage`. - * - On tab close or navigation away: The `pageReload` flag is removed from `localStorage`. - * - * Example Usage: - * This function is useful for determining whether to perform cleanup operations (like - * closing a database connection) only on tab close or navigation events but not on a refresh. - */ - private fun addBeforeUnloadListener() { - UI.getCurrent().page.executeJs( - """ - let isUnloading = false; - - window.addEventListener('beforeunload', function(event) { - isUnloading = true; - // Save a flag to detect page reload - if (performance.navigation.type === 1) { - // Page refresh detected - localStorage.setItem('pageReload', 'true'); - } else { - // Tab close or navigation away - localStorage.removeItem('pageReload'); - } - }); - - // Use 'unload' event as a fallback for closing/navigating away cases - window.addEventListener('unload', function(event) { - if (!isUnloading) { - localStorage.removeItem('pageReload'); - } - }); - """ - ) - } - /** * Adds a main window listener. * @param l the listener to be registered. diff --git a/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt b/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt index b4750cab1..78cb102de 100644 --- a/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt +++ b/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt @@ -252,10 +252,6 @@ abstract class VApplication(override val registry: Registry) : VerticalLayout(), mainWindow!!.setBookmarksMenu(DBookmarkMenu(menu!!)) mainWindow!!.setWorkspaceContextItemMenu(DBookmarkMenu(menu!!)) mainWindow!!.connectedUser = userName - mainWindow!!.addWindowDetachListener { - //closing DB connection if the UI is closed after 3 heartbeats - closeConnection() - } } fun remove(mainWindow: MainWindow?) { diff --git a/gradle.properties b/gradle.properties index 3ef638654..4a8a8e00c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx4g # group=org.kopi -version=1.5.12 +version=1.5.12-02TI-SNAPSHOT From 5d9c781a31147d2f226969ed66059bf0f298bf75 Mon Sep 17 00:00:00 2001 From: mgrati Date: Fri, 2 Jan 2026 17:14:12 +0100 Subject: [PATCH 2/3] Fix : Link database connection life cycle management to VaadinSession life cycle : Clear leaked connection on session destroy [APPS-02TI] --- .../visual/ui/vaadin/visual/VApplication.kt | 90 ++++++++++++++++++- .../src/main/webapp/WEB-INF/web.xml | 2 +- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt b/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt index 78cb102de..09e495a32 100644 --- a/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt +++ b/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2013-2022 kopiLeft Services SARL, Tunis TN - * Copyright (c) 1990-2022 kopiRight Managed Solutions GmbH, Wien AT + * Copyright (c) 2013-2025 kopiLeft Services SARL, Tunis TN + * Copyright (c) 1990-2025 kopiRight Managed Solutions GmbH, Wien AT * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,8 +18,12 @@ package org.kopi.galite.visual.ui.vaadin.visual import java.sql.SQLException +import java.time.LocalDateTime import java.util.Date import java.util.Locale +import java.util.concurrent.CopyOnWriteArrayList + +import org.slf4j.LoggerFactory import com.vaadin.flow.component.AttachEvent import com.vaadin.flow.component.UI @@ -33,6 +37,8 @@ import com.vaadin.flow.router.PreserveOnRefresh import com.vaadin.flow.server.AppShellRegistry import com.vaadin.flow.server.AppShellSettings import com.vaadin.flow.server.ServiceInitEvent +import com.vaadin.flow.server.SessionDestroyEvent +import com.vaadin.flow.server.SessionInitEvent import com.vaadin.flow.server.VaadinServiceInitListener import com.vaadin.flow.server.VaadinServlet import com.vaadin.flow.server.VaadinSession @@ -337,6 +343,9 @@ abstract class VApplication(override val registry: Registry) : VerticalLayout(), if (dBConnection == null) { throw SQLException(MessageCode.getMessage("VIS-00054")) } else { + // Add initiated database connection to vaadin session attributes. + // This allows us to clean up / close opened database connections on session destroy event. + addConnectionToSession(dBConnection!!) // set query trace level setTraceLevel() } @@ -396,6 +405,31 @@ abstract class VApplication(override val registry: Registry) : VerticalLayout(), } } + /** + * Add initiated connection on login to current vaadin session. + * This allows us to clean up / close opened connections on session destroy event. + * + * @param connection Database connection initiated on application login. + */ + fun addConnectionToSession(connection: Connection) { + val session = VaadinSession.getCurrent() + + session.lock() + try { + // Get the existing list, or create a new one if null + val connections = session.getAttribute("DB_CONNECTIONS") as? MutableList + ?: CopyOnWriteArrayList().also { + // CopyOnWriteArrayList is used to be thread-safe, + // since multiple tabs/requests may access the list concurrently. + session.setAttribute("DB_CONNECTIONS", it) + } + // Add the new connection to the session attribute. + connections.add(connection) + } finally { + session.unlock() + } + } + /** * Attaches a window to this application. * @param window The window to be added. @@ -663,6 +697,8 @@ class GaliteAppShellConfigurator: AppShellConfigurator { } class ApplicationServiceInitListener: VaadinServiceInitListener { + private val logger = LoggerFactory.getLogger("vaadin.session.lifecycle") + override fun serviceInit(event: ServiceInitEvent) { val context = event.source.context val appShellRegistry = AppShellRegistry.getInstance(context) @@ -678,5 +714,55 @@ class ApplicationServiceInitListener: VaadinServiceInitListener { val loadingIndicatorConfiguration = uiInitEvent.ui.loadingIndicatorConfiguration loadingIndicatorConfiguration.firstDelay = 1000 } + + // Add logging when a new vaadin session is initiated. + event.source.addSessionInitListener(::onSessionInit) + // Close all database connections opened within the current vaadin session. + event.source.addSessionDestroyListener(::onSessionDestroy) + } + + /** + * Trace the created session ID when a new vaadin session is initiated. + */ + fun onSessionInit(event: SessionInitEvent) { + val session = event.session + + // FIXME !!! This next line is only added for test purposes. It changes the session timeout to 5 minutes (default 30 minutes). + // FIXME !!! Must remove before merging to master. + session.session.maxInactiveInterval = 5 * 60 + + logger.info("${LocalDateTime.now()} - New Session [ID : ${session.session.id}] is created.") + } + + /** + * Close all database connections opened within the current vaadin session if not already closed. + */ + fun onSessionDestroy(event: SessionDestroyEvent) { + val session = event.session + + session.lock() + try { + val connections = session.getAttribute("DB_CONNECTIONS") as? List + + connections?.forEach { connection -> + try { + // Close all opened connections in the current vaadin session. + if (!connection.poolConnection.isClosed) { + connection.poolConnection.close() + logger.info("${LocalDateTime.now()} - DB connection ${connection.poolConnection} closed " + + "for session [ID : ${session.session.id}].") + } + } catch (ex: Exception) { + logger.warn("${LocalDateTime.now()} - Failed to close connection ${connection.poolConnection} " + + "for session [ID : ${session.session.id}]", ex) + } + } + // Clear the "DB_CONNECTIONS" attribute. + session.setAttribute("DB_CONNECTIONS", null) + // Session destroyed. + logger.info("${LocalDateTime.now()} - Session [ID : ${session.session.id}] is destroyed.") + } finally { + session.unlock() + } } } diff --git a/galite-demo/galite-vaadin/src/main/webapp/WEB-INF/web.xml b/galite-demo/galite-vaadin/src/main/webapp/WEB-INF/web.xml index 9764b875c..f47eca96f 100644 --- a/galite-demo/galite-vaadin/src/main/webapp/WEB-INF/web.xml +++ b/galite-demo/galite-vaadin/src/main/webapp/WEB-INF/web.xml @@ -50,6 +50,6 @@ /* - 480 + 300 From 88c7fb66a87f3313f0497cb8692f03654e5041b6 Mon Sep 17 00:00:00 2001 From: mgrati Date: Thu, 8 Jan 2026 19:40:12 +0100 Subject: [PATCH 3/3] Fix : Remove debug code [APPS-02TI] --- .../org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt b/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt index 09e495a32..1d910ab70 100644 --- a/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt +++ b/galite-core/src/main/kotlin/org/kopi/galite/visual/ui/vaadin/visual/VApplication.kt @@ -727,10 +727,6 @@ class ApplicationServiceInitListener: VaadinServiceInitListener { fun onSessionInit(event: SessionInitEvent) { val session = event.session - // FIXME !!! This next line is only added for test purposes. It changes the session timeout to 5 minutes (default 30 minutes). - // FIXME !!! Must remove before merging to master. - session.session.maxInactiveInterval = 5 * 60 - logger.info("${LocalDateTime.now()} - New Session [ID : ${session.session.id}] is created.") }