account-lookup-service 17.10.2 → 17.11.0-snapshot.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.10.3](https://github.com/mojaloop/account-lookup-service/compare/v17.10.2...v17.10.3) (2025-06-24)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * ensure timeout callbacks have content-type headers ([#555](https://github.com/mojaloop/account-lookup-service/issues/555)) ([3850241](https://github.com/mojaloop/account-lookup-service/commit/38502419d1900e5ccef869ddde07274974efb62d))
11
+
5
12
  ### [17.10.2](https://github.com/mojaloop/account-lookup-service/compare/v17.10.1...v17.10.2) (2025-06-23)
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",
@@ -24,15 +24,18 @@
24
24
  },
25
25
  "PROTOCOL_VERSIONS": {
26
26
  "CONTENT": {
27
- "DEFAULT": "1.1",
27
+ "DEFAULT": "2.0",
28
28
  "VALIDATELIST": [
29
+ "2.0",
29
30
  "1.1",
30
31
  "1.0"
31
32
  ]
32
33
  },
33
34
  "ACCEPT": {
34
- "DEFAULT": "1",
35
+ "DEFAULT": "2",
35
36
  "VALIDATELIST": [
37
+ "2",
38
+ "2.0",
36
39
  "1",
37
40
  "1.0",
38
41
  "1.1"
@@ -76,7 +79,19 @@
76
79
  "DISABLED": false,
77
80
  "TIMEXP": "*/10 * * * * *",
78
81
  "TIMEZONE": "UTC",
79
- "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
+ }
80
95
  }
81
96
  },
82
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.2",
4
+ "version": "17.11.0-snapshot.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.2",
98
- "@mojaloop/central-services-stream": "11.7.0",
98
+ "@mojaloop/central-services-shared": "18.29.0-snapshot.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
  }
@@ -31,9 +31,10 @@ const { decodePayload } = require('@mojaloop/central-services-shared').Util.Stre
31
31
  const { initStepState } = require('../../../lib/util')
32
32
  const { createCallbackHeaders } = require('../../../lib/headers')
33
33
  const { ERROR_MESSAGES } = require('../../../constants')
34
+ const { makeAcceptContentTypeHeader } = require('@mojaloop/central-services-shared').Util.Headers
34
35
 
35
36
  const { FspEndpointTypes, FspEndpointTemplates } = Enum.EndPoints
36
- const { Headers, RestMethods } = Enum.Http
37
+ const { Headers, RestMethods, HeaderResources } = Enum.Http
37
38
 
38
39
  /**
39
40
  * @typedef {Object} PartiesDeps
@@ -246,10 +247,15 @@ class BasePartiesService {
246
247
  return cbHeaders
247
248
  }
248
249
 
249
- static createHubErrorCallbackHeaders (hubName, destination) {
250
+ static createHubErrorCallbackHeaders (hubName, destination, config) {
250
251
  return {
251
252
  [Headers.FSPIOP.SOURCE]: hubName,
252
- [Headers.FSPIOP.DESTINATION]: destination
253
+ [Headers.FSPIOP.DESTINATION]: destination,
254
+ [Headers.GENERAL.CONTENT_TYPE.value]: makeAcceptContentTypeHeader(
255
+ HeaderResources.PARTIES,
256
+ config.PROTOCOL_VERSIONS.CONTENT.DEFAULT.toString(),
257
+ config.API_TYPE
258
+ )
253
259
  }
254
260
  }
255
261
 
@@ -41,7 +41,7 @@ class TimeoutPartiesService extends PutPartiesErrorService {
41
41
  */
42
42
  static createInstance (deps, cacheKey, spanName) {
43
43
  const { destination, partyType, partyId } = TimeoutPartiesService.parseExpiredKey(cacheKey)
44
- const headers = TimeoutPartiesService.createHubErrorCallbackHeaders(deps.config.HUB_NAME, destination)
44
+ const headers = TimeoutPartiesService.createHubErrorCallbackHeaders(deps.config.HUB_NAME, destination, deps.config)
45
45
  const params = { Type: partyType, ID: partyId } // todo: think, if we need to handle party SubId
46
46
  const childSpan = createSpan(spanName, headers, params)
47
47
 
@@ -34,10 +34,12 @@ 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
@@ -45,24 +47,26 @@ const timeout = async (options) => {
45
47
 
46
48
  try {
47
49
  isRunning = true
50
+ if (!await distLock?.acquireLock()) return
48
51
  await TimeoutService.timeoutInterschemePartiesLookups(options)
49
52
  await TimeoutService.timeoutProxyGetPartiesLookups(options)
50
53
  logger.verbose('ALS timeout handler is done')
51
54
  } catch (err) {
52
55
  logger.error('error in timeout: ', err)
53
56
  } finally {
57
+ await distLock?.releaseLock()
54
58
  isRunning = false
55
59
  }
56
60
  }
57
61
 
58
62
  const register = async (options) => {
59
63
  if (Config.HANDLERS_TIMEOUT_DISABLED) return false
60
-
61
64
  const { logger } = options
62
65
 
63
66
  try {
64
67
  if (isRegistered) {
65
- await stop()
68
+ logger.info('Timeout handler already registered')
69
+ return false
66
70
  }
67
71
  timeoutJob = CronJob.from({
68
72
  start: false,
@@ -70,6 +74,7 @@ const register = async (options) => {
70
74
  cronTime: Config.HANDLERS_TIMEOUT_TIMEXP,
71
75
  timeZone: Config.HANDLERS_TIMEOUT_TIMEZONE
72
76
  })
77
+ distLock = createDistLock(Config.HANDLERS_TIMEOUT?.DIST_LOCK, logger)
73
78
  timeoutJob.start()
74
79
  isRegistered = true
75
80
  logger.info('Timeout handler registered')
@@ -83,7 +88,8 @@ const register = async (options) => {
83
88
  const stop = async () => {
84
89
  if (isRegistered) {
85
90
  await timeoutJob.stop()
86
- isRegistered = undefined
91
+ // await distLock?.releaseLock()
92
+ isRegistered = false
87
93
  }
88
94
  }
89
95
 
@@ -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) => {
package/src/lib/config.js CHANGED
@@ -162,6 +162,7 @@ const config = {
162
162
  HANDLERS_TIMEOUT_TIMEXP: RC.HANDLERS.TIMEOUT.TIMEXP,
163
163
  HANDLERS_TIMEOUT_TIMEZONE: RC.HANDLERS.TIMEOUT.TIMEZONE,
164
164
  HANDLERS_TIMEOUT_BATCH_SIZE: RC.HANDLERS.TIMEOUT.BATCH_SIZE,
165
+ HANDLERS_TIMEOUT_DIST_LOCK_ENABLED: RC.HANDLERS.TIMEOUT?.DIST_LOCK?.enabled,
165
166
  ERROR_HANDLING: RC.ERROR_HANDLING,
166
167
  SWITCH_ENDPOINT: RC.SWITCH_ENDPOINT,
167
168
  INSTRUMENTATION_METRICS_DISABLED: RC.INSTRUMENTATION.METRICS.DISABLED,
@@ -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
  }
@@ -91,10 +91,12 @@ describe('Timeout Handler', () => {
91
91
  expect(await checkKeysExistence(keys)).toEqual([0, 0])
92
92
 
93
93
  expect(history.length).toBe(2)
94
- const path0 = history.find(h => h.path.includes(partyIds[0])).path
95
- const path1 = history.find(h => h.path.includes(partyIds[1])).path
96
- expect(path0).toBe(`/parties/${PARTY_ID_TYPE}/${partyIds[0]}/error`)
97
- expect(path1).toBe(`/parties/${PARTY_ID_TYPE}/${partyIds[1]}/error`)
94
+ const entry0 = history.find(h => h.path.includes(partyIds[0]))
95
+ const entry1 = history.find(h => h.path.includes(partyIds[1]))
96
+ expect(entry0.path).toBe(`/parties/${PARTY_ID_TYPE}/${partyIds[0]}/error`)
97
+ expect(entry1.path).toBe(`/parties/${PARTY_ID_TYPE}/${partyIds[1]}/error`)
98
+ expect(entry0.headers['content-type']).toContain('parties')
99
+ expect(entry1.headers['content-type']).toContain('parties')
98
100
  })
99
101
 
100
102
  it('should pass timeoutProxyGetPartiesLookups flow', async () => {
@@ -122,9 +124,11 @@ describe('Timeout Handler', () => {
122
124
  expect(await checkKeysExistence(keys)).toEqual([0, 0])
123
125
 
124
126
  expect(history.length).toBe(2)
125
- const path1 = history.find(h => h.path.includes(partyId1)).path
126
- const path2 = history.find(h => h.path.includes(partyId2)).path
127
- expect(path1).toBe(`/parties/${PARTY_ID_TYPE}/${partyId1}/error`)
128
- expect(path2).toBe(`/parties/${PARTY_ID_TYPE}/${partyId2}/error`)
127
+ const entry1 = history.find(h => h.path.includes(partyId1))
128
+ const entry2 = history.find(h => h.path.includes(partyId2))
129
+ expect(entry1.path).toBe(`/parties/${PARTY_ID_TYPE}/${partyId1}/error`)
130
+ expect(entry2.path).toBe(`/parties/${PARTY_ID_TYPE}/${partyId2}/error`)
131
+ expect(entry1.headers['content-type']).toContain('parties')
132
+ expect(entry2.headers['content-type']).toContain('parties')
129
133
  })
130
134
  })
@@ -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,20 @@ describe('TimeoutHandler', () => {
57
80
  })
58
81
 
59
82
  describe('timeout', () => {
83
+ afterEach(async () => {
84
+ await TimeoutHandler.stop()
85
+ })
86
+
60
87
  it('should execute timout 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)
96
+ Config.HANDLERS_TIMEOUT_DIST_LOCK_ENABLED = false
67
97
  jest.spyOn(TimeoutService, 'timeoutInterschemePartiesLookups').mockImplementation(async () => {
68
98
  await wait(1000)
69
99
  })
@@ -73,6 +103,30 @@ describe('TimeoutHandler', () => {
73
103
  ])
74
104
  expect(TimeoutService.timeoutInterschemePartiesLookups).toHaveBeenCalledTimes(1)
75
105
  })
106
+
107
+ it('should use distributed lock when enabled', async () => {
108
+ Config.HANDLERS_TIMEOUT.DIST_LOCK = { enabled: true }
109
+ jest.spyOn(TimeoutService, 'timeoutProxyGetPartiesLookups').mockResolvedValue()
110
+ await TimeoutHandler.register(mockOptions)
111
+
112
+ await TimeoutHandler.timeout(mockOptions)
113
+ expect(mockDistLock.acquire).toHaveBeenCalledTimes(1)
114
+ expect(mockDistLock.release).toHaveBeenCalledTimes(1)
115
+ expect(TimeoutService.timeoutProxyGetPartiesLookups).toHaveBeenCalled()
116
+ })
117
+
118
+ it('should not run if distributed lock cannot be acquired', async () => {
119
+ mockDistLock.acquire = jest.fn().mockResolvedValue(false)
120
+ Config.HANDLERS_TIMEOUT.DIST_LOCK = { enabled: true }
121
+ jest.spyOn(TimeoutService, 'timeoutProxyGetPartiesLookups').mockResolvedValue()
122
+ await TimeoutHandler.register(mockOptions)
123
+
124
+ await TimeoutHandler.timeout(mockOptions)
125
+
126
+ expect(mockDistLock.acquire).toHaveBeenCalledTimes(1)
127
+ expect(mockDistLock.release).toHaveBeenCalled() // todo: think if we need .release() to have been called
128
+ expect(TimeoutService.timeoutProxyGetPartiesLookups).not.toHaveBeenCalled()
129
+ })
76
130
  })
77
131
 
78
132
  describe('register', () => {