Message-ID: From: "robert-mirzakhanian (@robert-mirzakhanian)" To: "pgjdbc/pgjdbc" Date: Mon, 03 Feb 2025 21:19:38 +0000 Subject: Re: [pgjdbc/pgjdbc] PR #3506: feat: Add access for global host status tracker In-Reply-To: References: List-Id: X-GitHub-Author-Login: robert-mirzakhanian X-GitHub-Comment-Id: 2632100192 X-GitHub-Comment-Type: issue_comment X-GitHub-Edited-At: 2025-02-03T21:20:15Z X-GitHub-Issue: 3506 X-GitHub-Repo: pgjdbc/pgjdbc X-GitHub-Type: comment X-GitHub-Url: https://github.com/pgjdbc/pgjdbc/pull/3506#issuecomment-2632100192 Content-Type: text/plain; charset=utf-8 > so I still don't understand how this is supposed to be used? So, i wrote small example with test. Hope this makes the idea of using it clearer. Example: ```kotlin package ru.github.rmirzakhanian import com.zaxxer.hikari.HikariDataSource import org.postgresql.hostchooser.GlobalHostStatusTracker import org.postgresql.hostchooser.HostStatus import org.postgresql.util.HostSpec class CloudHandler( private val bestHost: String, private val client: Client, private val secondaryDataSource: HikariDataSource ) { enum class Status { ALIVE, DEAD, UNKNOWN } enum class Role { Primary, Secondary } enum class DataCenter { SSS, VVV, KKK } data class HostInfo(val host: String, val status: Status, val role: Role, val dc: DataCenter) class Client { fun getInfo(): List { return listOf( // HostInfo("host1", Status.ALIVE, Role.Primary, DataCenter.SSS), // HostInfo("host2", Status.ALIVE, Role.Secondary, DataCenter.VVV), // HostInfo("host3", Status.ALIVE, Role.Secondary, DataCenter.KKK) ) } } private var wasDown = false //periodic calls from background thread fun checkHosts() { val host = client.getInfo().find { it.host == bestHost } ?: return if (host.status != Status.ALIVE) { wasDown = true } else if (wasDown) { val hostStatus = GlobalHostStatusTracker.getHostStatusMap()[HostSpec(bestHost, 5432)] if (hostStatus == HostStatus.ConnectFail) { secondaryDataSource.hikariPoolMXBean.softEvictConnections() } wasDown = false } } } ``` Test ```kotlin import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariPoolMXBean import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.Test import org.postgresql.hostchooser.GlobalHostStatusTracker import org.postgresql.hostchooser.HostStatus import org.postgresql.util.HostSpec import ru.github.rmirzakhanian.CloudHandler import ru.github.rmirzakhanian.CloudHandler.DataCenter import ru.github.rmirzakhanian.CloudHandler.HostInfo import ru.github.rmirzakhanian.CloudHandler.Role import ru.github.rmirzakhanian.CloudHandler.Status class CloudHandlerTest { // The microservice stores information about its current data center location and sorts database connections based on low to high latency. // About the test: Imagine that the microservice is already running and has established a connection // to the database jdbc:postgresql://host2:5433,host1:5432,host3:5434/some_db // host2 @Test fun testHostRecovery() { // Test client that returns the next status on each call to getInfo(). val client = mockk() // Define a sequence of statuses: // 1st call: ALIVE, 2nd call: DEAD, 3rd call: ALIVE (return to normal state) every { client.getInfo() } returnsMany listOf( listOf( HostInfo("host1", Status.ALIVE, Role.Primary, DataCenter.SSS), HostInfo("host2", Status.ALIVE, Role.Secondary, DataCenter.VVV), HostInfo("host3", Status.ALIVE, Role.Secondary, DataCenter.KKK) ), listOf( HostInfo("host1", Status.ALIVE, Role.Primary, DataCenter.SSS), HostInfo("host2", Status.DEAD, Role.Secondary, DataCenter.VVV), HostInfo("host3", Status.ALIVE, Role.Secondary, DataCenter.KKK) ), listOf( HostInfo("host1", Status.ALIVE, Role.Primary, DataCenter.SSS), HostInfo("host2", Status.ALIVE, Role.Secondary, DataCenter.VVV), HostInfo("host3", Status.ALIVE, Role.Secondary, DataCenter.KKK) ) ) val mockkDataSource = mockk() val mockkHikariPoolMXBean = mockk() every { mockkDataSource.hikariPoolMXBean } returns mockkHikariPoolMXBean every { mockkHikariPoolMXBean.softEvictConnections() } returns Unit // Create a CloudHandler instance with the test client. val handler = CloudHandler("host2", client, mockkDataSource) // After create datasource's to primary database and secondary database. // Postgres report two records in GlobalHostStatusTracker. GlobalHostStatusTracker.reportHostStatus(HostSpec("host1", 5432), HostStatus.Primary) GlobalHostStatusTracker.reportHostStatus(HostSpec("host2", 5432), HostStatus.Secondary) // Simulate sequential calls to checkHosts(), as if they occur every 30 seconds. handler.checkHosts() // 1st call: status is ALIVE, wasDown flag remains false. // During operation, host 2 shuts down to service the server. // The Postgres driver understands this and switches to the next replica in the list. Now it is host3 // After switches Postgres driver report 2 rows in GlobalHostStatusTracker. GlobalHostStatusTracker.reportHostStatus(HostSpec("host2", 5432), HostStatus.ConnectFail) GlobalHostStatusTracker.reportHostStatus(HostSpec("host3", 5432), HostStatus.Secondary) handler.checkHosts() // 2nd call: status is DEAD, wasDown flag becomes true. // Waiting some calls when host2 return to normal state... handler.checkHosts() // 10th call: status is ALIVE, recovery detected, mockkHikariPoolMXBean.softEvictConnections should be called. // Checking mockkHikariPoolMXBean.softEvictConnections called exactly once verify(exactly = 1) { mockkHikariPoolMXBean.softEvictConnections() } } } ```