account-lookup-service 17.10.3 → 17.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [17.11.0](https://github.com/mojaloop/account-lookup-service/compare/v17.10.3...v17.11.0) (2025-07-07)
6
+
7
+
8
+ ### Features
9
+
10
+ * **csi-1604:** used ha timeout design with redlock impl ([#556](https://github.com/mojaloop/account-lookup-service/issues/556)) ([17a2477](https://github.com/mojaloop/account-lookup-service/commit/17a24779fb39b09ee535cd908f4f30ab45e57a8e))
11
+
5
12
  ### [17.10.3](https://github.com/mojaloop/account-lookup-service/compare/v17.10.2...v17.10.3) (2025-06-24)
6
13
 
7
14
 
@@ -80,7 +80,20 @@
80
80
  "DISABLED": false,
81
81
  "TIMEXP": "*/30 * * * * *",
82
82
  "TIMEZONE": "UTC",
83
- "BATCH_SIZE": 100
83
+ "BATCH_SIZE": 100,
84
+ "DIST_LOCK": {
85
+ "enabled": false,
86
+ "lockTimeout": 10000,
87
+ "acquireTimeout": 5000,
88
+ "driftFactor": 0.01,
89
+ "retryCount": 3,
90
+ "retryDelay": 200,
91
+ "retryJitter": 100,
92
+ "redisConfigs": [{
93
+ "type": "redis-cluster",
94
+ "cluster": [{ "host": "localhost", "port": 6379 }]
95
+ }]
96
+ }
84
97
  }
85
98
  },
86
99
  "SWITCH_ENDPOINT": "http://localhost:3001",
@@ -79,7 +79,19 @@
79
79
  "DISABLED": false,
80
80
  "TIMEXP": "*/10 * * * * *",
81
81
  "TIMEZONE": "UTC",
82
- "BATCH_SIZE": 100
82
+ "BATCH_SIZE": 100,
83
+ "DIST_LOCK": {
84
+ "enabled": true,
85
+ "lockTimeout": 10000,
86
+ "driftFactor": 0.01,
87
+ "retryCount": 3,
88
+ "retryDelay": 200,
89
+ "retryJitter": 100,
90
+ "redisConfigs": [{
91
+ "type": "redis-cluster",
92
+ "cluster": [{ "host": "redis-node-0", "port": 6379 }]
93
+ }]
94
+ }
83
95
  }
84
96
  },
85
97
  "SWITCH_ENDPOINT": "http://central-ledger:3001",
@@ -68,6 +68,8 @@ services:
68
68
  - "sh"
69
69
  - "-c"
70
70
  - "node src/handlers/index.js h --timeout"
71
+ environment:
72
+ - LOG_LEVEL=debug
71
73
  depends_on:
72
74
  account-lookup-service:
73
75
  condition: service_healthy
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "account-lookup-service",
3
3
  "description": "Account Lookup Service is used to validate Party and Participant lookups.",
4
- "version": "17.10.3",
4
+ "version": "17.11.0",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ModusBox",
7
7
  "contributors": [
@@ -68,6 +68,7 @@
68
68
  "seed:run": "knex seed:run $npm_package_config_knex",
69
69
  "seed:create": "knex seed:make $npm_package_config_knex",
70
70
  "regenerate": "yo swaggerize:test --framework hapi --apiPath './config/api_swagger.json'",
71
+ "dc:up:als-toh": ". ./test/integration/env.sh && docker compose $npm_package_config_env_file up account-lookup-service-handlers",
71
72
  "dc:build": "docker compose $npm_package_config_env_file build",
72
73
  "dc:up": ". ./test/integration/env.sh && docker compose $npm_package_config_env_file up -d && docker ps",
73
74
  "dc:down": ". ./test/integration/env.sh && docker compose $npm_package_config_env_file down -v",
@@ -94,13 +95,13 @@
94
95
  "@mojaloop/central-services-health": "15.1.0",
95
96
  "@mojaloop/central-services-logger": "11.9.0",
96
97
  "@mojaloop/central-services-metrics": "12.6.0",
97
- "@mojaloop/central-services-shared": "18.28.3",
98
- "@mojaloop/central-services-stream": "11.7.0",
98
+ "@mojaloop/central-services-shared": "18.29.0",
99
+ "@mojaloop/central-services-stream": "11.8.0",
99
100
  "@mojaloop/database-lib": "11.2.0",
100
101
  "@mojaloop/event-sdk": "14.6.1",
101
102
  "@mojaloop/inter-scheme-proxy-cache-lib": "2.6.0",
102
103
  "@mojaloop/ml-schema-transformer-lib": "2.7.1",
103
- "@mojaloop/sdk-standard-components": "19.15.2",
104
+ "@mojaloop/sdk-standard-components": "19.16.0",
104
105
  "@now-ims/hapi-now-auth": "2.1.0",
105
106
  "ajv": "8.17.1",
106
107
  "ajv-keywords": "5.1.0",
@@ -165,7 +166,7 @@
165
166
  "axios": "1.10.0",
166
167
  "axios-retry": "^4.5.0",
167
168
  "docdash": "2.0.2",
168
- "dotenv": "^16.5.0",
169
+ "dotenv": "^17.0.1",
169
170
  "get-port": "5.1.1",
170
171
  "ioredis-mock": "^8.9.0",
171
172
  "jest": "29.7.0",
package/src/constants.js CHANGED
@@ -43,8 +43,11 @@ const HANDLER_TYPES = Object.freeze({
43
43
  TIMEOUT: 'timeout'
44
44
  })
45
45
 
46
+ const TIMEOUT_HANDLER_DIST_LOCK_KEY = 'mutex:als-timeout-handler'
47
+
46
48
  module.exports = {
47
49
  API_TYPES,
48
50
  ERROR_MESSAGES,
49
- HANDLER_TYPES
51
+ HANDLER_TYPES,
52
+ TIMEOUT_HANDLER_DIST_LOCK_KEY
50
53
  }
@@ -34,35 +34,41 @@ const CronJob = require('cron').CronJob
34
34
  const ErrorHandler = require('@mojaloop/central-services-error-handling')
35
35
  const TimeoutService = require('../domain/timeout')
36
36
  const Config = require('../lib/config')
37
+ const { createDistLock } = require('../lib')
37
38
 
38
39
  let timeoutJob
39
40
  let isRegistered
40
41
  let isRunning
42
+ let distLock
41
43
 
42
44
  const timeout = async (options) => {
43
45
  if (isRunning) return
44
46
  const { logger } = options
47
+ let isAcquired = false
45
48
 
46
49
  try {
47
50
  isRunning = true
51
+ isAcquired = await distLock?.acquireLock()
52
+ if (!isAcquired) return
48
53
  await TimeoutService.timeoutInterschemePartiesLookups(options)
49
54
  await TimeoutService.timeoutProxyGetPartiesLookups(options)
50
55
  logger.verbose('ALS timeout handler is done')
51
56
  } catch (err) {
52
57
  logger.error('error in timeout: ', err)
53
58
  } finally {
59
+ if (isAcquired) await distLock.releaseLock()
54
60
  isRunning = false
55
61
  }
56
62
  }
57
63
 
58
64
  const register = async (options) => {
59
65
  if (Config.HANDLERS_TIMEOUT_DISABLED) return false
60
-
61
66
  const { logger } = options
62
67
 
63
68
  try {
64
69
  if (isRegistered) {
65
- await stop()
70
+ logger.info('Timeout handler already registered')
71
+ return false
66
72
  }
67
73
  timeoutJob = CronJob.from({
68
74
  start: false,
@@ -70,6 +76,7 @@ const register = async (options) => {
70
76
  cronTime: Config.HANDLERS_TIMEOUT_TIMEXP,
71
77
  timeZone: Config.HANDLERS_TIMEOUT_TIMEZONE
72
78
  })
79
+ distLock = createDistLock(Config.HANDLERS_TIMEOUT?.DIST_LOCK, logger)
73
80
  timeoutJob.start()
74
81
  isRegistered = true
75
82
  logger.info('Timeout handler registered')
@@ -83,7 +90,8 @@ const register = async (options) => {
83
90
  const stop = async () => {
84
91
  if (isRegistered) {
85
92
  await timeoutJob.stop()
86
- isRegistered = undefined
93
+ await distLock?.releaseLock()
94
+ isRegistered = false
87
95
  }
88
96
  }
89
97
 
@@ -70,7 +70,7 @@ const registerHandlers = async (handlers, options) => {
70
70
  const registerAllHandlers = async (options) => {
71
71
  options.logger.debug('Registering all handlers')
72
72
  await init(options)
73
- TimeoutHandler.register(options)
73
+ await TimeoutHandler.register(options)
74
74
  }
75
75
 
76
76
  const stopAllHandlers = async (options) => {
@@ -0,0 +1,74 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2025 Mojaloop Foundation
5
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are 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.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * Mojaloop Foundation
23
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
24
+
25
+ --------------
26
+ ******/
27
+
28
+ const { distLock } = require('@mojaloop/central-services-shared').Util
29
+ const { TIMEOUT_HANDLER_DIST_LOCK_KEY } = require('../constants')
30
+
31
+ const createDistLock = (distLockConfig, logger) => {
32
+ const distLockKey = TIMEOUT_HANDLER_DIST_LOCK_KEY
33
+ const distLockTtl = distLockConfig?.lockTimeout || 10000
34
+ const distLockAcquireTimeout = distLockConfig?.acquireTimeout || 5000
35
+
36
+ const lock = distLockConfig?.enabled
37
+ ? distLock.createLock(distLockConfig, logger)
38
+ : null
39
+
40
+ const acquireLock = async () => {
41
+ if (lock) {
42
+ try {
43
+ return !!(await lock.acquire(distLockKey, distLockTtl, distLockAcquireTimeout))
44
+ } catch (err) {
45
+ logger.error('error acquiring distributed lock:', err)
46
+ // should this be added to metrics?
47
+ return false
48
+ }
49
+ }
50
+ logger.info('distributed lock not configured or disabled, running without distributed lock')
51
+ return true
52
+ }
53
+
54
+ const releaseLock = async () => {
55
+ if (lock) {
56
+ try {
57
+ await lock.release()
58
+ logger.verbose('distributed lock released')
59
+ } catch (error) {
60
+ logger.error('error releasing distributed lock:', error)
61
+ // should this be added to metrics?
62
+ }
63
+ } else {
64
+ logger.verbose('distributed lock not configured or disabled')
65
+ }
66
+ }
67
+
68
+ return {
69
+ acquireLock,
70
+ releaseLock
71
+ }
72
+ }
73
+
74
+ module.exports = createDistLock
package/src/lib/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  const { loggerFactory, asyncStorage } = require('@mojaloop/central-services-logger/src/contextLogger')
2
2
  const { TransformFacades } = require('@mojaloop/ml-schema-transformer-lib')
3
+ const createDistLock = require('./createDistLock')
3
4
 
4
5
  const logger = loggerFactory('ALS') // global logger without context
5
6
 
6
7
  module.exports = {
7
8
  logger,
8
9
  asyncStorage,
9
- TransformFacades
10
+ TransformFacades,
11
+ createDistLock
10
12
  }
@@ -31,11 +31,23 @@
31
31
 
32
32
  'use strict'
33
33
 
34
+ let mockDistLock
35
+ jest.mock('@mojaloop/central-services-shared', () => ({
36
+ ...jest.requireActual('@mojaloop/central-services-shared'),
37
+ Util: {
38
+ ...jest.requireActual('@mojaloop/central-services-shared').Util,
39
+ distLock: {
40
+ createLock: jest.fn().mockImplementation(() => mockDistLock)
41
+ }
42
+ }
43
+ }))
44
+
34
45
  const CronJob = require('cron').CronJob
35
46
  const TimeoutHandler = require('../../../src/handlers/TimeoutHandler')
36
47
  const TimeoutService = require('../../../src/domain/timeout')
37
48
  const Config = require('../../../src/lib/config')
38
49
  const { logger } = require('../../../src/lib')
50
+
39
51
  const DefaultConfig = { ...Config }
40
52
 
41
53
  const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
@@ -48,8 +60,19 @@ describe('TimeoutHandler', () => {
48
60
  start: jest.fn(),
49
61
  stop: jest.fn()
50
62
  })
51
- const mockProxyCache = { processExpiredAlsKeys: jest.fn() }
52
- mockOptions = { proxyCache: mockProxyCache, batchSize: 10, logger }
63
+ const mockProxyCache = {
64
+ processExpiredAlsKeys: jest.fn(),
65
+ processExpiredProxyGetPartiesKeys: jest.fn()
66
+ }
67
+ mockOptions = {
68
+ proxyCache: mockProxyCache,
69
+ batchSize: 10,
70
+ logger
71
+ }
72
+ mockDistLock = {
73
+ acquire: jest.fn().mockResolvedValue(true),
74
+ release: jest.fn().mockResolvedValue()
75
+ }
53
76
  })
54
77
 
55
78
  afterEach(async () => {
@@ -57,13 +80,19 @@ describe('TimeoutHandler', () => {
57
80
  })
58
81
 
59
82
  describe('timeout', () => {
60
- it('should execute timout service', async () => {
83
+ afterEach(async () => {
84
+ await TimeoutHandler.stop()
85
+ })
86
+
87
+ it('should execute timeout service', async () => {
88
+ await TimeoutHandler.register(mockOptions)
61
89
  jest.spyOn(TimeoutService, 'timeoutInterschemePartiesLookups').mockResolvedValue()
62
- await expect(TimeoutHandler.timeout(mockOptions)).resolves.toBeUndefined()
90
+ await TimeoutHandler.timeout(mockOptions)
63
91
  expect(TimeoutService.timeoutInterschemePartiesLookups).toHaveBeenCalled()
64
92
  })
65
93
 
66
- it('should not run if isRunning is true', async () => {
94
+ it('should not run if isRunning is true (distLock disabled)', async () => {
95
+ await TimeoutHandler.register(mockOptions)
67
96
  jest.spyOn(TimeoutService, 'timeoutInterschemePartiesLookups').mockImplementation(async () => {
68
97
  await wait(1000)
69
98
  })
@@ -73,6 +102,30 @@ describe('TimeoutHandler', () => {
73
102
  ])
74
103
  expect(TimeoutService.timeoutInterschemePartiesLookups).toHaveBeenCalledTimes(1)
75
104
  })
105
+
106
+ it('should use distributed lock when enabled', async () => {
107
+ Config.HANDLERS_TIMEOUT.DIST_LOCK = { enabled: true }
108
+ jest.spyOn(TimeoutService, 'timeoutProxyGetPartiesLookups').mockResolvedValue()
109
+ await TimeoutHandler.register(mockOptions)
110
+
111
+ await TimeoutHandler.timeout(mockOptions)
112
+ expect(mockDistLock.acquire).toHaveBeenCalledTimes(1)
113
+ expect(mockDistLock.release).toHaveBeenCalledTimes(1)
114
+ expect(TimeoutService.timeoutProxyGetPartiesLookups).toHaveBeenCalled()
115
+ })
116
+
117
+ it('should not run if distributed lock cannot be acquired', async () => {
118
+ mockDistLock.acquire = jest.fn().mockResolvedValue(false)
119
+ Config.HANDLERS_TIMEOUT.DIST_LOCK = { enabled: true }
120
+ jest.spyOn(TimeoutService, 'timeoutProxyGetPartiesLookups').mockResolvedValue()
121
+ await TimeoutHandler.register(mockOptions)
122
+
123
+ await TimeoutHandler.timeout(mockOptions)
124
+
125
+ expect(mockDistLock.acquire).toHaveBeenCalledTimes(1)
126
+ expect(mockDistLock.release).not.toHaveBeenCalled()
127
+ expect(TimeoutService.timeoutProxyGetPartiesLookups).not.toHaveBeenCalled()
128
+ })
76
129
  })
77
130
 
78
131
  describe('register', () => {