Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(condo): INFRA-202 multiple database sources #4618

Merged
merged 39 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
320d78d
test(keystone): INFRA-202 add default test for custom db config
sitozzz Apr 3, 2024
91814d9
chore(keystone): INFRA-202 init custom knex adapter for multiple conn…
sitozzz Apr 5, 2024
07499ab
fix(keystone): INFRA-202 send transactions to master db only
sitozzz Apr 8, 2024
1f4d958
fix(global): INFRA-202 change common places of custom adapter usage
sitozzz Apr 9, 2024
98246d7
fix(keystone): INFRA-202 add ability to init different knex adapters …
sitozzz Apr 11, 2024
28d396e
fix(condo): INFRA-202 wrap custom adapter usage with adapter function
sitozzz Apr 15, 2024
60de7da
chore(keystone): INFRA-202 remove unnecessary todo
sitozzz Apr 24, 2024
e378779
fix(keystone): INFRA-202 add detailed error message
sitozzz Apr 25, 2024
1142da3
fix(keystone): INFRA-202 remove test env error throw
sitozzz Apr 25, 2024
4647ff1
fix(global): INFRA-202 add checkout for sql file download
sitozzz Apr 25, 2024
4353891
fix(global): INFRA-202 create replication template
sitozzz Apr 25, 2024
f1a9be0
fix(global): INFRA-202 temp disable adapter cache for ci tests
sitozzz Apr 25, 2024
fad65f1
fix(keystone): INFRA-202 try to access list adapters with custom db a…
sitozzz May 2, 2024
ba49033
fix(condo): INFRA-202 remove todo note from .env.example
sitozzz May 2, 2024
c5984ef
fix(keystone): INFRA-202 move keystone list adapter access to externa…
sitozzz May 3, 2024
84300ae
fix(global): INFRA-202 unstage temp kmigrator changes
sitozzz May 3, 2024
6ec8849
fix(keystone): INFRA-202 update hc db access due to adapter API changes
sitozzz May 6, 2024
3fbf775
fix(keystone): INFRA-202 rollback kmigrator changes
sitozzz May 6, 2024
7b63038
fix(keystone): INFRA-202 revert knex pool setting to safe value
sitozzz May 23, 2024
974cfed
fix(keystone): INFRA-202 try to figure out what's going on with clien…
sitozzz May 23, 2024
467de99
fix(keystone): INFRA-202 add execution time logging
sitozzz May 24, 2024
f0bc987
fix(keystone): INFRA-202 add execution time logging
sitozzz May 24, 2024
9e0ad7d
fix(global): INFRA-202 remove logs from adapter
sitozzz Jun 5, 2024
83bf8fa
chore(global): INFRA-202 add adapter specific logs & move helm to fin…
sitozzz Jun 24, 2024
64eb83c
fix(global): INFRA-200 add images to werf config
sitozzz Jun 25, 2024
1e6def2
refactor(keystone): INFRA-202 rename connections & provide more speci…
sitozzz Jun 25, 2024
629e40e
fix(keystone): INFRA-202 extend mutable operations config at ReplicaK…
sitozzz Jun 27, 2024
b6eee8d
fix(keystone): INFRA-202 add del alias & provide debug logging for mi…
sitozzz Jul 2, 2024
e22dd3d
fix(global): INFRA-200 rid off ini file configuration & remove debug …
sitozzz Jul 3, 2024
1f15928
fix(global): INFRA-202 rebase with origin
sitozzz Jul 5, 2024
1c2a313
fix(condo): INFRA-202 try to enable custom db adapter for test runs
sitozzz Jul 8, 2024
ed63338
fix(condo): INFRA-202 try to enable custom db adapter for test runs
sitozzz Jul 8, 2024
d3baf9e
fix(condo): INFRA-202 try to enable custom db adapter for test runs
sitozzz Jul 8, 2024
3a35656
fix(condo): INFRA-202 try to enable custom db adapter for test runs
sitozzz Jul 8, 2024
1e6cd44
fix(condo): INFRA-202 try to enable custom db adapter for test runs
sitozzz Jul 8, 2024
6ce2657
refactor(global): INFRA-202 move replication env setting to program a…
sitozzz Jul 9, 2024
fc98db6
chore(global): INFRA-202 rollback ci target policy
sitozzz Jul 10, 2024
4475408
fix(global): INFRA-202 update helm submodule to master
sitozzz Jul 10, 2024
7cb7dcd
fix(global): INFRA-202 prepare script fixed
sitozzz Jul 10, 2024
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
2 changes: 1 addition & 1 deletion .helm
Submodule .helm updated from 9ab928 to df8d9d
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const isoWeek = require('dayjs/plugin/isoWeek')
const get = require('lodash/get')
const groupBy = require('lodash/groupBy')

const { getDatabaseAdapter } = require('@open-condo/keystone/databaseAdapters/utils')
const { getSchemaCtx } = require('@open-condo/keystone/schema')

const { GqlToKnexBaseAdapter } = require('@condo/domains/common/utils/serverSchema/GqlToKnexBaseAdapter')
Expand Down Expand Up @@ -47,7 +48,7 @@ class TicketGqlToKnexAdapter extends GqlToKnexBaseAdapter {
async loadData () {
this.result = null
const { keystone } = await getSchemaCtx(this.domainName)
const knex = keystone.adapter.knex
const { knex } = getDatabaseAdapter(keystone)

this.whereIn = Object.fromEntries(this.whereIn)
// create whereIn structure [['property_id', 'user_id'], [['some_property_id', 'some_user_id'], ...]]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { get, omit, find } = require('lodash')

const { getDatabaseAdapter } = require('@open-condo/keystone/databaseAdapters/utils')
const { getSchemaCtx } = require('@open-condo/keystone/schema')

const { AbstractDataLoader } = require('@condo/domains/analytics/utils/services/dataLoaders/AbstractDataLoader')
Expand All @@ -14,8 +15,7 @@ class IncidentPropertyGqlKnexLoader extends GqlToKnexBaseAdapter {

async loadData () {
const { keystone } = await getSchemaCtx(this.domainName)
const knex = keystone.adapter.knex

const { knex } = getDatabaseAdapter(keystone)

const propertyFilter = get(find(this.where, 'property', {}), 'property.id_in', [])
const incidentFilter = get(find(this.where, 'incident', {}), 'incident.id_in', [])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const Big = require('big.js')
const { get, pick, isEmpty } = require('lodash')

const { getDatabaseAdapter } = require('@open-condo/keystone/databaseAdapters/utils')
const { getSchemaCtx } = require('@open-condo/keystone/schema')

const { PAYMENT_DONE_STATUS, PAYMENT_WITHDRAWN_STATUS } = require('@condo/domains/acquiring/constants/payment')
Expand All @@ -16,7 +17,7 @@ class BillingResidentKnexLoader extends GqlToKnexBaseAdapter {

async loadData () {
const { keystone } = await getSchemaCtx(this.domainName)
const knex = keystone.adapter.knex
const { knex } = getDatabaseAdapter(keystone)

const propertyIds = get(this.where, 'property.id_in', [])

Expand Down Expand Up @@ -55,7 +56,7 @@ class PaymentGqlKnexLoader extends GqlToKnexBaseAdapter {
async loadData () {
this.result = null
const { keystone } = await getSchemaCtx(this.domainName)
const knex = keystone.adapter.knex
const { knex } = getDatabaseAdapter(keystone)

this.extendAggregationWithFilter(this.aggregateBy)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const Big = require('big.js')
const dayjs = require('dayjs')
const { get, omit } = require('lodash')

const { getDatabaseAdapter } = require('@open-condo/keystone/databaseAdapters/utils')
const { getSchemaCtx } = require('@open-condo/keystone/schema')

const { AbstractDataLoader } = require('@condo/domains/analytics/utils/services/dataLoaders/AbstractDataLoader')
Expand All @@ -23,7 +24,7 @@ class ReceiptGqlKnexLoader extends GqlToKnexBaseAdapter {
this.result = null

const { keystone } = await getSchemaCtx(this.domainName)
const knex = keystone.adapter.knex
const { knex } = getDatabaseAdapter(keystone)

this.whereIn = {}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { get, isEmpty } = require('lodash')

const { getDatabaseAdapter } = require('@open-condo/keystone/databaseAdapters/utils')
const { getSchemaCtx } = require('@open-condo/keystone/schema')

const { AbstractDataLoader } = require('@condo/domains/analytics/utils/services/dataLoaders/AbstractDataLoader')
Expand All @@ -15,7 +16,7 @@ class ResidentGqlKnexLoader extends GqlToKnexBaseAdapter {

async loadData () {
const { keystone } = await getSchemaCtx(this.domainName)
const knex = keystone.adapter.knex
const { knex } = getDatabaseAdapter(keystone)

this.whereIn = {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const dayjs = require('dayjs')
const { get, isEmpty, find } = require('lodash')

const conf = require('@open-condo/config')
const { getDatabaseAdapter } = require('@open-condo/keystone/databaseAdapters/utils')
const { getSchemaCtx } = require('@open-condo/keystone/schema')
const { extractReqLocale } = require('@open-condo/locales/extractReqLocale')
const { i18n } = require('@open-condo/locales/loader')
Expand Down Expand Up @@ -236,7 +237,7 @@ class TicketQualityControlGqlLoader extends GqlToKnexBaseAdapter {
this.result = null

const { keystone } = await getSchemaCtx(this.domainName)
const knex = keystone.adapter.knex
const { knex } = getDatabaseAdapter(keystone)

this.extendAggregationWithFilter(this.aggregateBy)

Expand Down
3 changes: 2 additions & 1 deletion apps/condo/domains/common/utils/serverSchema/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { getItems } = require('@keystonejs/server-side-graphql-client')
const { isFunction } = require('lodash')

const { getDatabaseAdapter } = require('@open-condo/keystone/databaseAdapters/utils')
const { getSchemaCtx } = require('@open-condo/keystone/schema')
const GLOBAL_QUERY_LIMIT = 1000

Expand Down Expand Up @@ -93,7 +94,7 @@ class GqlWithKnexLoadList {
async initContext () {
const { keystone: modelAdapter } = await getSchemaCtx(this.listKey)
this.keystone = modelAdapter
this.knex = modelAdapter.adapter.knex
this.knex = getDatabaseAdapter(modelAdapter).knex
}

// Takes rawAggregate SQL function and apply it on all objects with id from ids
Expand Down
9 changes: 8 additions & 1 deletion bin/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const CERT_FILE = path.join(__filename, '..', '.ssl', 'localhost.pem')

program.option('-f, --filter <names...>', 'Filters apps by name')
program.option('--https', 'Uses https for local running')
program.option('-r, --replication <names...>', 'Enables replica adapter to interact with multiple databases')
program.description(`Prepares applications from the /apps directory for local running
by creating separate databases for them
and running their local bin/prepare.js scripts.
Expand All @@ -35,7 +36,7 @@ function logWithIndent (message, indent = 1) {

async function prepare () {
program.parse()
const { https, filter } = program.opts()
const { https, filter, replication } = program.opts()

// Step 1. Sanity checks
logWithIndent('Running sanity checks')
Expand Down Expand Up @@ -108,6 +109,12 @@ async function prepare () {
SPORT: String(app.sport),
SERVER_URL: app.serviceUrl,
}

if (replication && app in replication) {
env.DATABASE_URL = `custom:{"default":{"read":"postgresql://postgres:postgres@127.0.0.1:5433/${app.pgName}","write":"postgresql://postgres:postgres@127.0.0.1:5432/${app.pgName}"}}` // NOSONAR used only for test purposes
env.DATABASE_MAPPING = '[{"match":"*","query":"default","command":"default"}]'
}

await prepareAppEnv(app.name, env)
logWithIndent('Running migration script', 2)
const migrateResult = await runAppPackageJsonScript(app.name, 'migrate')
Expand Down
9 changes: 4 additions & 5 deletions packages/keystone/adapterCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
const { get, cloneDeep, floor, isEqual } = require('lodash')
const LRUCache = require('lru-cache')

const { getListAdapters } = require('@open-condo/keystone/databaseAdapters/utils')

const { getExecutionContext } = require('./executionContext')
const { getLogger } = require('./logging')
const Metrics = require('./metrics')
Expand Down Expand Up @@ -251,12 +253,9 @@ class AdapterCache {
* @returns {Promise<void>}
*/
async function patchKeystoneWithAdapterCache (keystone, cacheAPI) {
const keystoneAdapter = keystone.adapter

const cache = cacheAPI.cache
const excludedLists = cacheAPI.excludedLists

const listAdapters = Object.values(keystoneAdapter.listAdapters)
const listAdapters = Object.values(getListAdapters(keystone))

// Step 1: Preprocess lists.
const relations = {} // list -> [{list, path, many}]
Expand Down Expand Up @@ -512,4 +511,4 @@ function queryIsComplex (query, list, relations) {

module.exports = {
AdapterCache,
}
}
117 changes: 117 additions & 0 deletions packages/keystone/databaseAdapters/ReplicaKnexAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
const { KnexAdapter } = require('@keystonejs/adapter-knex')
const { knex } = require('knex')
const { get, omit } = require('lodash')

const { getLogger } = require('@open-condo/keystone/logging')

const logger = getLogger('replicaKnexAdapter')

const IMMUTABLE_OPERATIONS = ['select', 'show']

/**
*
*/
class ReplicaKnexAdapter extends KnexAdapter {
constructor (props) {
super(omit(props, ['connection']))
this.readConnection = get(props, ['connection', 'read'])
this.writeConnection = get(props, ['connection', 'write'])
}

async _connect () {
this.knex = knex({
SavelevMatthew marked this conversation as resolved.
Show resolved Hide resolved
client: 'postgres',
pool: { min: 0, max: 1 },
connection: this.writeConnection,
})

this.knexWrite = knex({
client: 'postgres',
pool: { min: 0, max: 3 },
connection: this.writeConnection,
})

this.knexRead = knex({
client: 'postgres',
pool: { min: 0, max: 3 },
connection: this.readConnection,
})

this.knex.context.transaction = (...props) => {
return this.knexWrite.context.transaction(...props)
}

const checkUseMasterSingle = (object) => {
// if object.method equals "insert", "delete" or "update" then use master endpoint
if (object.method !== undefined) {
return !IMMUTABLE_OPERATIONS.includes(object.method)
}
// try to parse sql
return !IMMUTABLE_OPERATIONS.some(sub => object.sql.includes(sub))
}

const checkUseMasterMultiple = (array) => array.some(checkUseMasterSingle)

//override runner method
this.knex.client.runner = (builder) => {
if (builder._queryContext && builder._queryContext.useMaster === true) {
return this.knexWrite.client.runner(builder)
} else if (
builder._queryContext &&
builder._queryContext.useMaster === false
) {
return this.knexRead.client.runner(builder)
}

let sql
let useMaster = true
try {
sql = builder.toSQL()
useMaster = Array.isArray(sql)
? checkUseMasterMultiple(sql)
: checkUseMasterSingle(sql)
} catch (error) {
logger.error({
err: error,
msg: 'catch error at connection determination',
})
// swallow this, it will be thrown properly in a second when Knex internally runs it
}

return useMaster
? this.knexWrite.client.runner(builder)
: this.knexRead.client.runner(builder)
}

const masterConnectionResult = await this.knexWrite.raw('select 1+1 as result').catch(result => ({ error: result.error || result }))
const readConnectionResult = await this.knexRead.raw('select 1+1 as result').catch(result => ({ error: result.error || result }))

if (masterConnectionResult.error || readConnectionResult.error) {
if (masterConnectionResult.error) {
logger.error({
err: masterConnectionResult.error,
msg: 'Could not connect to master database',
})
throw masterConnectionResult.error
}

logger.error({
err: readConnectionResult.error,
msg: 'Could not connect to replica database',
})
throw readConnectionResult.error
}

return true
}

disconnect () {
this.knexWrite.destroy()
this.knexRead.destroy()
this.knex.destroy()
}
}

module.exports = {
ReplicaKnexAdapter,
}
19 changes: 11 additions & 8 deletions packages/keystone/databaseAdapters/ScalableDatabaseAdapter.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
const { KnexAdapter } = require('@keystonejs/adapter-knex')
const { BaseKeystoneAdapter } = require('@keystonejs/keystone')
const { find } = require('lodash/collection')
const { mapValues } = require('lodash/object')
const { find, mapValues } = require('lodash')

const { FakeDatabaseAdapter } = require('./FakeDatabaseAdapter')
const { ReplicaKnexAdapter } = require('./ReplicaKnexAdapter')
const { parseDatabaseUrl, parseDatabaseMapping, matchDatabase } = require('./utils')


function initDatabaseAdapters (databases) {
return mapValues(databases, initDatabaseAdapter)
}

function initDatabaseAdapter (databaseUrl) {
if (databaseUrl.startsWith('postgresql:')) {
if (typeof databaseUrl === 'object') {
return new ReplicaKnexAdapter({ connection: databaseUrl })
} else if (databaseUrl.startsWith('postgresql:')) {
return new KnexAdapter({ knexOptions: { connection: databaseUrl } })
} else if (databaseUrl.startsWith('fake:')) {
// NOTE: case for testing!
Expand All @@ -35,15 +38,15 @@ function initListAdapter (rootAdapter, parentAdapter, key, adapterConfig) {

class ScalableDatabaseAdapter extends BaseKeystoneAdapter {
static PUBLIC_API = [
'name', 'config', // base keystone adapter props
'newListAdapter', 'getListAdapterByKey', 'connect', '_connect', 'checkDatabaseVersion', 'postConnect', 'disconnect', // keystone interface props
'__databaseAdapters', '__listMappingRule', '__listMappingAdapters', '__listToDatabase', // own private props
'__kmigratorKnexAdapters', // kmigrator hacks for backward compatibility
'name', 'config', // base keystone adapter props
'newListAdapter', 'getListAdapterByKey', 'connect', '_connect', 'checkDatabaseVersion', 'postConnect', 'disconnect', // keystone interface props
'__databaseAdapters', '__listMappingRule', '__listMappingAdapters', '__listToDatabase', // own private props
'__kmigratorKnexAdapters', // kmigrator hacks for backward compatibility
]

constructor (opts = {}) {
if (!opts.url || !opts.url.startsWith('custom:')) throw new Error('ScalableDatabaseAdapter({ url }) wrong url format!')
if (!opts.mapping || !opts.mapping.startsWith('[')) throw new Error('ScalableDatabaseAdapter({ mapping }) wrong url format!')
if (!opts.mapping || !opts.mapping.startsWith('[')) throw new Error(`ScalableDatabaseAdapter({ mapping }) wrong url format! ${opts.mapping}`)

const databases = parseDatabaseUrl(opts.url)
if (!databases) throw new Error('ScalableDatabaseAdapter({ url }) wrong url format!')
Expand Down
Loading
Loading