Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.smeup.dbnative

/**
* Resolves the most specific [ConnectionConfig] matching [fileName].
*/
fun findConnectionConfigFor(fileName: String, connectionsConfig: List<ConnectionConfig>): ConnectionConfig {
val configList = connectionsConfig.filter {
it.fileName.equals(fileName, ignoreCase = true) || it.fileName == "*" ||
fileName.uppercase().matches(Regex(it.fileName.uppercase().replace("*", ".*")))
}
require(configList.isNotEmpty()) {
"Wrong configuration. Not found a ConnectionConfig entry matching name: $fileName"
}
return configList.sortedWith(ConnectionConfigComparator())[0]
}

/**
* Orders connection patterns from most specific to least specific.
*/
class ConnectionConfigComparator : Comparator<ConnectionConfig> {
override fun compare(o1: ConnectionConfig?, o2: ConnectionConfig?): Int {
require(o1 != null)
require(o2 != null)
return when {
o1.fileName == "*" && o2.fileName != "*" -> 1
o1.fileName != "*" && o2.fileName == "*" -> -1
o1.fileName.contains("*") && o2.fileName.contains("*") -> o1.fileName.compareTo(o2.fileName)
o1.fileName.contains("*") && !o2.fileName.contains("*") -> 1
!o1.fileName.contains("*") && o2.fileName.contains("*") -> -1
else -> o1.fileName.compareTo(o2.fileName)
}
}
}
75 changes: 75 additions & 0 deletions base/src/main/kotlin/com/smeup/dbnative/ConnectionProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.smeup.dbnative

/**
* Provides thread-scoped access to [DBMManager] instances.
*/
object ConnectionProvider {

private val threadLocal = ThreadLocal<MutableMap<ConnectionConfig, DBMManager>>()

@Volatile private var config: DBNativeAccessConfig? = null
@Volatile private var managerFactory: ((ConnectionConfig) -> DBMManager)? = null

/**
* Configures the provider with connection matching rules and a manager factory.
*/
fun configure(config: DBNativeAccessConfig, factory: (ConnectionConfig) -> DBMManager) {
this.config = config
this.managerFactory = factory
}

/**
* Returns `true` if [configure] (or [configureWithPool]) has been called.
*/
fun isConfigured(): Boolean = config != null

/**
* Functional interface used by [withScope] to execute a scoped block.
*/
fun interface ScopedBlock {
@Throws(Exception::class)
fun execute()
}

/**
* Runs [block] in a thread-local scope and closes all managers created in that scope.
*/
@Throws(Exception::class)
fun withScope(block: ScopedBlock) {
requireNotNull(config) { "ConnectionProvider not configured" }
threadLocal.set(mutableMapOf())
try {
block.execute()
} finally {
val managers = threadLocal.get()
threadLocal.remove()
managers?.values?.forEach { it.close() }
}
}

/**
* Returns the current scope manager for [fileName], creating it on first use.
*/
fun currentManager(fileName: String): DBMManager {
val managers = requireNotNull(threadLocal.get()) { "No active scope on this thread" }
val cfg = requireNotNull(config)
val factory = requireNotNull(managerFactory)
val connectionConfig = findConnectionConfigFor(fileName, cfg.connectionsConfig)
return managers.getOrPut(connectionConfig) { factory(connectionConfig) }
}

/**
* Like [currentManager], but returns `null` if there is no active scope or no match.
*/
fun currentManagerOrNull(fileName: String): DBMManager? {
val managers = threadLocal.get() ?: return null
val cfg = config ?: return null
val factory = managerFactory ?: return null
return try {
val connectionConfig = findConnectionConfigFor(fileName, cfg.connectionsConfig)
managers.getOrPut(connectionConfig) { factory(connectionConfig) }
} catch (e: IllegalArgumentException) {
null
}
}
}
32 changes: 21 additions & 11 deletions base/src/main/kotlin/com/smeup/dbnative/DBNativeAccessConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,37 @@ package com.smeup.dbnative

import com.smeup.dbnative.log.Logger

/**
* Configuration for DB native access.
*
* @param connectionsConfig List of available connection configurations.
* @param logger Optional logger implementation.
*/
data class DBNativeAccessConfig (val connectionsConfig: List<ConnectionConfig>, val logger: Logger? = null){
constructor(connectionsConfig: List<ConnectionConfig>):this(connectionsConfig, null)
}

/**
* Create a new instance of connection configuratio for a single file or file groups.
* @param fileName File or file group identifier, wildcard "*" is admitted.
* I.E. file=*tablename is for all files starts with <code>tablename</code>
* @param url Connection url, protocol could be customized
* @param user The user
* @param password The password
* @param driver If needed
* @param impl DBMManager implementation. If doesn't specified is assumed by url
* @param properties Others connection properties
* Creates a connection configuration for a single file or a file group.
*
* @param fileName File or file-group identifier. The `*` wildcard is supported
* (for example, `*tablename` matches files ending with `tablename`).
* @param url Connection URL. The protocol can be customized.
* @param user Username.
* @param password Password.
* @param driver JDBC driver class name, when required.
* @param impl DB manager implementation. If not specified, it is inferred from `url`.
* @param properties Additional connection properties.
* @param poolConfig Connection pool configuration.
* */
data class ConnectionConfig (
data class ConnectionConfig @JvmOverloads constructor(
val fileName: String,
val url: String,
val user: String,
val password: String,
val driver: String? = null,
val impl: String? = null,
val properties : Map<String, String> = mutableMapOf())
val properties: Map<String, String> = mutableMapOf(),
val poolConfig: PoolConfig? = null
)

20 changes: 20 additions & 0 deletions base/src/main/kotlin/com/smeup/dbnative/PoolConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.smeup.dbnative

/**
* Connection pool tuning values used by SQL-backed managers.
*
* @param maximumPoolSize Maximum number of active connections.
* @param minimumIdle Minimum number of idle connections kept in the pool.
* @param connectionTimeoutMs Maximum wait time in milliseconds to borrow a connection.
* @param idleTimeoutMs Maximum idle time in milliseconds before a connection can be evicted.
* @param maxLifetimeMs Maximum lifetime in milliseconds for a pooled connection.
*/
data class PoolConfig @JvmOverloads constructor(
val maximumPoolSize: Int = 10,
val minimumIdle: Int = 2,
val connectionTimeoutMs: Long = 30_000,
val idleTimeoutMs: Long = 600_000,
val maxLifetimeMs: Long = 1_800_000,
// Set when the JDBC driver does not implement Connection.isValid() (e.g. AS400).
val connectionTestQuery: String? = null
)
146 changes: 146 additions & 0 deletions base/src/test/kotlin/com/smeup/dbnative/ConnectionProviderTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.smeup.dbnative

import com.smeup.dbnative.file.DBFile
import com.smeup.dbnative.model.FileMetadata
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue

private val TEST_CONFIG = ConnectionConfig(
fileName = "*",
url = "class:com.smeup.dbnative.mock.MockDBManager",
user = "",
password = ""
)

private val TEST_CONFIG_B = ConnectionConfig(
fileName = "FILEB",
url = "class:com.smeup.dbnative.mock.MockDBManager",
user = "b",
password = "b"
)

private val ACCESS_CONFIG = DBNativeAccessConfig(listOf(TEST_CONFIG_B, TEST_CONFIG))

/**
* Unit tests for [ConnectionProvider] scoped lifecycle and lookup behavior.
*/
class ConnectionProviderTest {

@Before
fun setUp() {
ConnectionProvider.configure(ACCESS_CONFIG) { connConfig -> SpyDBMManager(connConfig) }
}

@After
fun tearDown() {
// Reset state between tests by re-configuring with a no-op factory;
// the real clean-up is handled by withScope's finally block.
}

@Test
fun withScope_createsManagerOnDemand() {
ConnectionProvider.withScope {
val manager = ConnectionProvider.currentManager("FILEA")
assertNotNull(manager)
}
}

@Test
fun withScope_sameManagerForSameFile() {
ConnectionProvider.withScope {
val m1 = ConnectionProvider.currentManager("FILEA")
val m2 = ConnectionProvider.currentManager("FILEA")
assertSame(m1, m2)
}
}

@Test
fun withScope_differentManagerForDifferentConfig() {
ConnectionProvider.withScope {
val mA = ConnectionProvider.currentManager("FILEA")
val mB = ConnectionProvider.currentManager("FILEB")
assertTrue(mA !== mB)
}
}

@Test
fun withScope_closesManagersOnExit() {
val spies = mutableListOf<SpyDBMManager>()
ConnectionProvider.configure(ACCESS_CONFIG) { connConfig ->
SpyDBMManager(connConfig).also { spies.add(it) }
}
ConnectionProvider.withScope {
ConnectionProvider.currentManager("FILEA")
ConnectionProvider.currentManager("FILEB")
}
assertTrue(spies.isNotEmpty())
assertTrue(spies.all { it.closed })
}

@Test
fun currentManagerOrNull_returnsNullOutsideScope() {
val result = ConnectionProvider.currentManagerOrNull("FILEA")
assertNull(result)
}

@Test
fun currentManagerOrNull_returnsNullForUnknownFile() {
val isolatedConfig = DBNativeAccessConfig(listOf(TEST_CONFIG_B))
ConnectionProvider.configure(isolatedConfig) { connConfig -> SpyDBMManager(connConfig) }
ConnectionProvider.withScope {
val result = ConnectionProvider.currentManagerOrNull("UNKNOWN_NO_WILDCARD")
assertNull(result)
}
}

@Test
fun withScope_throwsWhenNotConfigured() {
// Temporarily point to a fresh unconfigured-looking provider by using a local object
// We test via reflection-like approach: re-create a scenario where configure was not called.
// Since ConnectionProvider is a singleton, we configure it with null-equivalent by
// configuring with an empty config and verifying the require message.
val emptyConfig = DBNativeAccessConfig(listOf())
ConnectionProvider.configure(emptyConfig) { SpyDBMManager(it) }

assertFailsWith<IllegalArgumentException> {
ConnectionProvider.withScope {
ConnectionProvider.currentManager("ANYTHING")
}
}
}

@Test
fun withScope_scopeIsThreadLocal() {
var managerSeenFromOtherThread: DBMManager? = null
ConnectionProvider.withScope {
val thread = Thread {
managerSeenFromOtherThread = ConnectionProvider.currentManagerOrNull("FILEA")
}
thread.start()
thread.join()
}
assertNull(managerSeenFromOtherThread)
}
}

/**
* Test double used to verify manager creation and closure semantics.
*/
class SpyDBMManager(override val connectionConfig: ConnectionConfig) : DBMManager {
var closed = false

override fun existFile(name: String) = false
override fun registerMetadata(metadata: FileMetadata, overwrite: Boolean) {}
override fun metadataOf(name: String): FileMetadata = throw NotImplementedError()
override fun openFile(name: String): DBFile = throw NotImplementedError()
override fun closeFile(name: String) {}
override fun unregisterMetadata(name: String) {}
override fun validateConfig() {}
override fun close() { closed = true }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.smeup.dbnative.manager

import com.smeup.dbnative.ConnectionProvider
import com.smeup.dbnative.DBNativeAccessConfig
import com.smeup.dbnative.log.Logger

/**
* Configures [ConnectionProvider] using the manager module factory.
*
* @param config Native access configuration.
* @param logger Optional logger forwarded to manager creation.
*/
fun ConnectionProvider.configure(config: DBNativeAccessConfig, logger: Logger? = null) {
configure(config) { connConfig -> createDBManager(connConfig, logger) }
}
Loading