jspurefix 5.10.6 → 5.11.1
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/README.md +46 -0
- package/dist/runtime/session-launcher.d.ts +2 -0
- package/dist/runtime/session-launcher.js +13 -0
- package/dist/runtime/session-launcher.js.map +1 -1
- package/dist/transport/msg-application.d.ts +3 -0
- package/dist/transport/msg-application.js.map +1 -1
- package/dist/transport/tcp/connect-error.d.ts +2 -0
- package/dist/transport/tcp/connect-error.js +32 -0
- package/dist/transport/tcp/connect-error.js.map +1 -0
- package/dist/transport/tcp/recovering-tcp-initiator.d.ts +6 -0
- package/dist/transport/tcp/recovering-tcp-initiator.js +57 -16
- package/dist/transport/tcp/recovering-tcp-initiator.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator-connector.d.ts +2 -0
- package/dist/transport/tcp/tcp-initiator-connector.js +8 -3
- package/dist/transport/tcp/tcp-initiator-connector.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator.d.ts +1 -0
- package/dist/transport/tcp/tcp-initiator.js +11 -43
- package/dist/transport/tcp/tcp-initiator.js.map +1 -1
- package/jsfix.test_client.txt +62 -62
- package/jsfix.test_server.txt +72 -72
- package/package.json +1 -1
- package/src/runtime/session-launcher.ts +25 -3
- package/src/transport/msg-application.ts +16 -0
- package/src/transport/tcp/connect-error.ts +56 -0
- package/src/transport/tcp/recovering-tcp-initiator.ts +76 -14
- package/src/transport/tcp/tcp-initiator-connector.ts +6 -1
- package/src/transport/tcp/tcp-initiator.ts +19 -10
|
@@ -57,6 +57,7 @@ export abstract class SessionLauncher {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
private acceptorEntity: FixEntity | null = null
|
|
60
|
+
private initiatorEntity: FixEntity | null = null
|
|
60
61
|
private stopRequested: boolean = false
|
|
61
62
|
|
|
62
63
|
protected async getAcceptor (sessionContainer: DependencyContainer): Promise<any> {
|
|
@@ -78,6 +79,11 @@ export abstract class SessionLauncher {
|
|
|
78
79
|
protected async getInitiator (sessionContainer: DependencyContainer): Promise<any> {
|
|
79
80
|
if (sessionContainer.isRegistered<FixEntity>(DITokens.FixEntity)) {
|
|
80
81
|
const entity = sessionContainer.resolve<FixEntity>(DITokens.FixEntity)
|
|
82
|
+
this.initiatorEntity = entity
|
|
83
|
+
if (this.stopRequested) {
|
|
84
|
+
this.logger.info('stop requested before the initiator started - not connecting')
|
|
85
|
+
return this.empty()
|
|
86
|
+
}
|
|
81
87
|
return entity.start()
|
|
82
88
|
} else {
|
|
83
89
|
return this.empty()
|
|
@@ -95,7 +101,8 @@ export abstract class SessionLauncher {
|
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
/**
|
|
98
|
-
* Stop what this launcher started -
|
|
104
|
+
* Stop what this launcher started - close the acceptor's listener, and give up a
|
|
105
|
+
* resilient initiator's attempts to get its transport back.
|
|
99
106
|
*
|
|
100
107
|
* An acceptor is meant to outlive any one counterparty, so nothing closes its
|
|
101
108
|
* listener on its own. That is right for a venue and wrong for an application
|
|
@@ -107,12 +114,18 @@ export abstract class SessionLauncher {
|
|
|
107
114
|
* usual arrangement as soon as more than one client is involved. There was then
|
|
108
115
|
* no way to shut a listener down short of killing the process.
|
|
109
116
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
117
|
+
* A resilient initiator had the same problem from the other side: losing the
|
|
118
|
+
* transport schedules another attempt, so it too kept the process alive with no way
|
|
119
|
+
* to call it off. See issue #72.
|
|
120
|
+
*
|
|
121
|
+
* Safe before start (the request is remembered and neither is opened), and safe to
|
|
122
|
+
* call twice. run() resolves once the listener has closed and the initiator has
|
|
123
|
+
* given up.
|
|
112
124
|
*/
|
|
113
125
|
public stop (): void {
|
|
114
126
|
this.stopRequested = true
|
|
115
127
|
this.stopAcceptor()
|
|
128
|
+
this.stopInitiator()
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
public async run (): Promise<boolean> {
|
|
@@ -229,6 +242,15 @@ export abstract class SessionLauncher {
|
|
|
229
242
|
}
|
|
230
243
|
}
|
|
231
244
|
|
|
245
|
+
// only a resilient initiator has anything to call off - a plain one ends with its
|
|
246
|
+
// session, so it has no stop to find here
|
|
247
|
+
private stopInitiator (): void {
|
|
248
|
+
const entity = this.initiatorEntity as any
|
|
249
|
+
if (entity && typeof entity.stop === 'function') {
|
|
250
|
+
entity.stop()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
232
254
|
private loadConfig (config: string | ISessionDescription): ISessionDescription {
|
|
233
255
|
if (typeof config === 'string') {
|
|
234
256
|
return require(path.join(this.root, config))
|
|
@@ -4,8 +4,24 @@ import { IHttpTransportDescription } from './http/http-transport-description'
|
|
|
4
4
|
export interface IMsgApplication {
|
|
5
5
|
readonly name: string
|
|
6
6
|
readonly type: string
|
|
7
|
+
/**
|
|
8
|
+
* An initiator which re-establishes its transport after the connection drops and
|
|
9
|
+
* resumes the same session, rather than ending when the socket does. Defaults to
|
|
10
|
+
* false, which gives a single connection. See "Reconnecting initiators" in the
|
|
11
|
+
* README for the retry policy below.
|
|
12
|
+
*/
|
|
7
13
|
readonly resilient: boolean
|
|
14
|
+
/** how long to wait between attempts whilst trying to establish a connection. Defaults to 5. */
|
|
8
15
|
readonly reconnectSeconds: number
|
|
16
|
+
/**
|
|
17
|
+
* how long to keep attempting a connection before giving up. Defaults to 60 for a
|
|
18
|
+
* resilient initiator and 22 for a plain one.
|
|
19
|
+
*/
|
|
20
|
+
readonly connectTimeoutSeconds?: number
|
|
21
|
+
/** resilient only: how long after losing the transport before trying to get it back. Defaults to 5. */
|
|
22
|
+
readonly recoveryAttemptSeconds?: number
|
|
23
|
+
/** resilient only: how long to wait after a failed recovery before trying again. Defaults to 30. */
|
|
24
|
+
readonly backoffFailConnectSeconds?: number
|
|
9
25
|
readonly tcp?: ITcpTransportDescription
|
|
10
26
|
readonly http?: IHttpTransportDescription
|
|
11
27
|
readonly protocol: string
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A readable reason for a failed connection.
|
|
3
|
+
*
|
|
4
|
+
* Node raises an AggregateError when the host resolved to more than one address and
|
|
5
|
+
* every attempt failed. That is not an edge case: 'localhost' is both ::1 and
|
|
6
|
+
* 127.0.0.1 on any modern machine, so it is what an ordinary refused connection looks
|
|
7
|
+
* like. Its own message is empty and the detail sits in errors[], so reporting
|
|
8
|
+
* e.message alone said nothing at all about the commonest failure there is - an
|
|
9
|
+
* application catching a rejected connect saw an Error with no message.
|
|
10
|
+
*
|
|
11
|
+
* Falls back to the code (ECONNREFUSED, ENOTFOUND, ETIMEDOUT) when there is no
|
|
12
|
+
* message anywhere, which is still enough to act on.
|
|
13
|
+
*/
|
|
14
|
+
export function describeConnectError (e: unknown): string {
|
|
15
|
+
if (e == null) return 'unknown error'
|
|
16
|
+
const described = detail(e)
|
|
17
|
+
return described.length > 0 ? described : String(e)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The reason, or '' when this error carries none.
|
|
22
|
+
*
|
|
23
|
+
* Kept separate from the public function so a nested error with nothing to say
|
|
24
|
+
* contributes nothing, rather than a stringified 'Error' that would then look like a
|
|
25
|
+
* reason and stop the aggregate falling back to its own code.
|
|
26
|
+
*/
|
|
27
|
+
function detail (e: unknown): string {
|
|
28
|
+
if (e == null) return ''
|
|
29
|
+
|
|
30
|
+
const err = e as { message?: string, code?: string, errors?: unknown[] }
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(err.errors) && err.errors.length > 0) {
|
|
33
|
+
// every address usually fails the same way - say it once rather than repeating
|
|
34
|
+
// 'connect ECONNREFUSED' for each family
|
|
35
|
+
const unique = Array.from(new Set(err.errors.map(detail).filter(m => m.length > 0)))
|
|
36
|
+
if (unique.length > 0) return unique.join('; ')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (err.message) return err.message
|
|
40
|
+
if (err.code) return err.code
|
|
41
|
+
return ''
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Give an error a message when it has none, keeping the object itself.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately not a wrapper: `code` and `errors` are what a caller switches on, and
|
|
48
|
+
* building a fresh Error to hold the text would throw both away. Returns the same
|
|
49
|
+
* error so it can be used inline.
|
|
50
|
+
*/
|
|
51
|
+
export function withConnectReason (e: Error): Error {
|
|
52
|
+
if (!e.message) {
|
|
53
|
+
e.message = describeConnectError(e)
|
|
54
|
+
}
|
|
55
|
+
return e
|
|
56
|
+
}
|
|
@@ -9,6 +9,7 @@ import { DITokens } from '../../runtime/di-tokens'
|
|
|
9
9
|
import { ITcpTransportDescription } from './tcp-transport-description'
|
|
10
10
|
import { IMsgApplication } from '../msg-application'
|
|
11
11
|
import { FixEntity } from '../fix-entity'
|
|
12
|
+
import { describeConnectError } from './connect-error'
|
|
12
13
|
|
|
13
14
|
/*
|
|
14
15
|
create one application session instance - and recover a lost transport. Hence, the application
|
|
@@ -25,8 +26,14 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
25
26
|
private initiator: TcpInitiator
|
|
26
27
|
private transport: MsgTransport
|
|
27
28
|
private th: Timeout | null = null
|
|
28
|
-
|
|
29
|
-
public
|
|
29
|
+
private stopRequested: boolean = false
|
|
30
|
+
public static readonly DefaultRecoveryAttemptSecs = 5
|
|
31
|
+
public static readonly DefaultBackoffFailConnectSecs = 30
|
|
32
|
+
public static readonly DefaultConnectTimeoutSecs = 60
|
|
33
|
+
|
|
34
|
+
public recoveryAttemptSecs: number = RecoveringTcpInitiator.DefaultRecoveryAttemptSecs
|
|
35
|
+
public backoffFailConnectSecs: number = RecoveringTcpInitiator.DefaultBackoffFailConnectSecs
|
|
36
|
+
public connectTimeoutSecs: number = RecoveringTcpInitiator.DefaultConnectTimeoutSecs
|
|
30
37
|
|
|
31
38
|
constructor (@inject(DITokens.IJsFixConfig) public readonly jsFixConfig: IJsFixConfig) {
|
|
32
39
|
super(jsFixConfig)
|
|
@@ -40,6 +47,14 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
40
47
|
if (!this.tcp) {
|
|
41
48
|
throw new Error('no tcp in session description need tcp { host: hostname, port: port }')
|
|
42
49
|
}
|
|
50
|
+
// the retry policy used to live only in these fields, which nothing set - an
|
|
51
|
+
// application could not change how hard or how long the engine tried to get its
|
|
52
|
+
// transport back without subclassing the launcher. See issue #72.
|
|
53
|
+
this.recoveryAttemptSecs = this.application.recoveryAttemptSeconds ?? this.recoveryAttemptSecs
|
|
54
|
+
this.backoffFailConnectSecs = this.application.backoffFailConnectSeconds ?? this.backoffFailConnectSecs
|
|
55
|
+
this.connectTimeoutSecs = this.application.connectTimeoutSeconds ?? this.connectTimeoutSecs
|
|
56
|
+
this.logger.info(`recovery policy: connectTimeout=${this.connectTimeoutSecs}s, ` +
|
|
57
|
+
`recoveryAttempt=${this.recoveryAttemptSecs}s, backoffFailConnect=${this.backoffFailConnectSecs}s`)
|
|
43
58
|
this.createSession(jsFixConfig)
|
|
44
59
|
}
|
|
45
60
|
|
|
@@ -54,6 +69,13 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
54
69
|
this.logger.info('session has permanently ended')
|
|
55
70
|
this.emit('end', this)
|
|
56
71
|
})
|
|
72
|
+
// A lost transport is this initiator's normal course of events, and recovery is
|
|
73
|
+
// driven by session.run() rejecting rather than by this event. But whilst waiting
|
|
74
|
+
// between attempts there is no run in flight to carry a listener, so a stop landing
|
|
75
|
+
// then would emit 'error' to nobody - which node treats as fatal.
|
|
76
|
+
this.session.on('error', (e: Error) => {
|
|
77
|
+
this.logger.info(`session error: ${e.message}`)
|
|
78
|
+
})
|
|
57
79
|
this.session.setState(SessionState.DisconnectedNoConnectionToday)
|
|
58
80
|
}
|
|
59
81
|
|
|
@@ -104,7 +126,31 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
104
126
|
// succeed in which case can restart session or fails in which case wait and
|
|
105
127
|
// restart an attempt to connect
|
|
106
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Give up recovering and end the session for good.
|
|
131
|
+
*
|
|
132
|
+
* Without this a resilient initiator could not be shut down: losing the transport
|
|
133
|
+
* schedules another attempt, and nothing cancelled that timer, so the process stayed
|
|
134
|
+
* alive retrying a counterparty the application had finished with. Safe to call
|
|
135
|
+
* twice, and safe whilst waiting between attempts with no transport at all.
|
|
136
|
+
*/
|
|
137
|
+
public stop (): void {
|
|
138
|
+
if (this.stopRequested) return
|
|
139
|
+
this.stopRequested = true
|
|
140
|
+
this.logger.info('stop requested - cancelling recovery')
|
|
141
|
+
this.clearTimer()
|
|
142
|
+
if (this.session.getState() !== SessionState.Stopped) {
|
|
143
|
+
this.session.requestStop('initiator stopped')
|
|
144
|
+
}
|
|
145
|
+
this.emit('end', this)
|
|
146
|
+
}
|
|
147
|
+
|
|
107
148
|
private recover (): void {
|
|
149
|
+
if (this.stopRequested) {
|
|
150
|
+
this.logger.info('transport lost after a stop was requested - not recovering')
|
|
151
|
+
this.emit('end', this)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
108
154
|
this.session.setState(SessionState.DetectBrokenNetworkConnection)
|
|
109
155
|
this.logger.info(`recover session transport - attempt in ${this.recoveryAttemptSecs} secs`)
|
|
110
156
|
if (!this.resetSeq()) {
|
|
@@ -115,10 +161,10 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
115
161
|
}
|
|
116
162
|
}
|
|
117
163
|
this.th = setTimeout(() => {
|
|
118
|
-
this.connect(
|
|
164
|
+
this.connect(this.connectTimeoutSecs).then(t => {
|
|
119
165
|
this.logger.info(`new transport ${t.id}`)
|
|
120
166
|
}).catch((e) => {
|
|
121
|
-
this.logger.info(`failed to re-connect ${e
|
|
167
|
+
this.logger.info(`failed to re-connect: ${describeConnectError(e)} - backoff for ${this.backoffFailConnectSecs}`)
|
|
122
168
|
this.th = setTimeout(() => {
|
|
123
169
|
this.logger.info('returning to recover()')
|
|
124
170
|
setImmediate(() => {
|
|
@@ -135,18 +181,34 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
135
181
|
|
|
136
182
|
// for first connection - reject if no initial connection established within timeout
|
|
137
183
|
// once connection established, will not resolve until session is ended - i.e. lost
|
|
138
|
-
// connections are re-established using the same session instance.
|
|
184
|
+
// connections are re-established using the same session instance. So this promise
|
|
185
|
+
// is the lifetime of the application, not a signal that the session is up: for that,
|
|
186
|
+
// use onReady on the session. See issue #72.
|
|
139
187
|
|
|
140
|
-
public async run (initialTimeout: number =
|
|
188
|
+
public async run (initialTimeout: number = this.connectTimeoutSecs): Promise<any> {
|
|
141
189
|
return await new Promise<any>((resolve, reject) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
190
|
+
// 'end' can arrive more than once - a stop both ends the session and calls off
|
|
191
|
+
// recovery, and each path announces itself - so settle only on the first. The
|
|
192
|
+
// listener goes on before connecting, not after: a stop landing whilst the first
|
|
193
|
+
// connection is still being attempted has to be able to end this too, and until
|
|
194
|
+
// now there was nothing listening that early to hear it.
|
|
195
|
+
let ended = false
|
|
196
|
+
const finish = (): void => {
|
|
197
|
+
if (ended) return
|
|
198
|
+
ended = true
|
|
199
|
+
this.clearTimer()
|
|
200
|
+
this.initiator?.end()
|
|
201
|
+
this.logger.info(`run: transport ${this.transport?.id ?? 'none'} ends - resolving`)
|
|
202
|
+
resolve(null)
|
|
203
|
+
}
|
|
204
|
+
this.on('end', finish)
|
|
205
|
+
|
|
206
|
+
this.connect(initialTimeout).catch(e => {
|
|
207
|
+
if (this.stopRequested) {
|
|
208
|
+
this.logger.info('run: first connection abandoned on stop - resolving')
|
|
209
|
+
finish()
|
|
210
|
+
return
|
|
211
|
+
}
|
|
150
212
|
this.logger.info(`run: failed to connect to first transport ${initialTimeout} - rejecting`)
|
|
151
213
|
reject(e)
|
|
152
214
|
})
|
|
@@ -9,8 +9,13 @@ import { FixEntity } from '../fix-entity'
|
|
|
9
9
|
|
|
10
10
|
@injectable()
|
|
11
11
|
export class TcpInitiatorConnector extends FixEntity {
|
|
12
|
+
public static readonly DefaultConnectTimeoutSecs = 22
|
|
13
|
+
|
|
14
|
+
public connectTimeoutSecs: number = TcpInitiatorConnector.DefaultConnectTimeoutSecs
|
|
15
|
+
|
|
12
16
|
constructor (@inject(DITokens.IJsFixConfig) public readonly config: IJsFixConfig) {
|
|
13
17
|
super(config)
|
|
18
|
+
this.connectTimeoutSecs = config.description.application?.connectTimeoutSeconds ?? this.connectTimeoutSecs
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
async start (reconnectTimeout: number = 0): Promise<any> {
|
|
@@ -62,7 +67,7 @@ export class TcpInitiatorConnector extends FixEntity {
|
|
|
62
67
|
const logger = this.config.logFactory.logger('initiator')
|
|
63
68
|
const initiator: FixInitiator = this.config.sessionContainer.resolve<FixInitiator>(TcpInitiator)
|
|
64
69
|
logger.info('connecting ...')
|
|
65
|
-
const initiatorTransport: MsgTransport = await initiator.connect(
|
|
70
|
+
const initiatorTransport: MsgTransport = await initiator.connect(this.connectTimeoutSecs)
|
|
66
71
|
logger.info('... connected, run session')
|
|
67
72
|
await initiatorSession.run(initiatorTransport)
|
|
68
73
|
logger.info('ends')
|
|
@@ -4,12 +4,12 @@ import { MsgTransport } from '../factory'
|
|
|
4
4
|
import { IJsFixConfig, IJsFixLogger } from '../../config'
|
|
5
5
|
import { TcpDuplex, FixDuplex } from '../duplex'
|
|
6
6
|
|
|
7
|
-
import * as util from 'util'
|
|
8
7
|
import { connect as tlsConnect, ConnectionOptions, TLSSocket } from 'tls'
|
|
9
8
|
import { createConnection } from 'net'
|
|
10
9
|
import Timeout = NodeJS.Timeout
|
|
11
10
|
import { TlsOptionsFactory } from './tls-options-factory'
|
|
12
11
|
import { describeConnectionOptions, describeNegotiated, describePeerCertificate } from './tls-diagnostics'
|
|
12
|
+
import { describeConnectError, withConnectReason } from './connect-error'
|
|
13
13
|
import { inject, injectable } from 'tsyringe'
|
|
14
14
|
import { DITokens } from '../../runtime/di-tokens'
|
|
15
15
|
import { ITcpTransportDescription } from './tcp-transport-description'
|
|
@@ -28,6 +28,7 @@ export class TcpInitiator extends FixInitiator {
|
|
|
28
28
|
private readonly logger: IJsFixLogger
|
|
29
29
|
private duplex: FixDuplex
|
|
30
30
|
private th: Timeout | null = null
|
|
31
|
+
private timeoutTh: Timeout | null = null
|
|
31
32
|
|
|
32
33
|
constructor (@inject(DITokens.IJsFixConfig) public readonly jsFixConfig: IJsFixConfig) {
|
|
33
34
|
super(jsFixConfig.description.application ?? null)
|
|
@@ -73,7 +74,7 @@ export class TcpInitiator extends FixInitiator {
|
|
|
73
74
|
// connect() whose timeout expires before the first retry interval fires - the
|
|
74
75
|
// default case, timeout and reconnectSeconds are both 5 - reported a generic
|
|
75
76
|
// "timeout whilst connecting" and threw away the reason. see #94.
|
|
76
|
-
this.logger.info(`first connect attempt failed: ${first
|
|
77
|
+
this.logger.info(`first connect attempt failed: ${describeConnectError(first)}`)
|
|
77
78
|
this.repeatConnect(timeoutSeconds, first)
|
|
78
79
|
.then((t: MsgTransport) => { resolve(t) })
|
|
79
80
|
.catch((e: Error) => { reject(e) })
|
|
@@ -189,13 +190,15 @@ export class TcpInitiator extends FixInitiator {
|
|
|
189
190
|
clearInterval(this.th)
|
|
190
191
|
this.th = null
|
|
191
192
|
}
|
|
193
|
+
if (this.timeoutTh) {
|
|
194
|
+
clearTimeout(this.timeoutTh)
|
|
195
|
+
this.timeoutTh = null
|
|
196
|
+
}
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
private async repeatConnect (timeoutSeconds: number, initialError?: Error): Promise < MsgTransport > {
|
|
195
200
|
return await new Promise<MsgTransport>(async (resolve, reject) => {
|
|
196
201
|
const application = this.application
|
|
197
|
-
const promisify = util.promisify
|
|
198
|
-
const timeoutPromise = promisify(setTimeout)
|
|
199
202
|
const reconnectSeconds = application?.reconnectSeconds ?? 5
|
|
200
203
|
let retries = 0
|
|
201
204
|
let lastError: Error | undefined = initialError
|
|
@@ -209,18 +212,24 @@ export class TcpInitiator extends FixInitiator {
|
|
|
209
212
|
resolve(t)
|
|
210
213
|
}).catch((e: Error) => {
|
|
211
214
|
lastError = e
|
|
212
|
-
this.logger.info(`${name}: retries ${retries} ${e
|
|
215
|
+
this.logger.info(`${name}: retries ${retries} ${describeConnectError(e)}`)
|
|
213
216
|
})
|
|
214
217
|
}, reconnectSeconds * 1000)
|
|
215
|
-
|
|
218
|
+
// a plain timer, not a promisified one: promisify(setTimeout) hands back no
|
|
219
|
+
// handle, so this deadline could never be cancelled. Connecting successfully
|
|
220
|
+
// cleared the retry interval and left this one running to its full term - up to
|
|
221
|
+
// the 60s an initiator allows - holding the event loop open long after the
|
|
222
|
+
// session was up, and stopping a process (or a test runner) from exiting.
|
|
223
|
+
this.timeoutTh = setTimeout(() => {
|
|
216
224
|
this.clearTimer()
|
|
217
225
|
this.state = InitiatorState.Stopped
|
|
218
|
-
|
|
226
|
+
// this error is what the application catches from run() - so give it a message
|
|
227
|
+
// it can print. An AggregateError arrives with none at all, which meant a
|
|
228
|
+
// caller handling a failed connect had nothing to report but an empty string.
|
|
229
|
+
const e = withConnectReason(lastError ?? new Error(`${name}: timeout of ${timeoutSeconds} whilst connecting`))
|
|
219
230
|
this.logger.warning(`${name}: giving up after ${timeoutSeconds}s and ${retries} retries, last error: ${e.message}`)
|
|
220
231
|
reject(e)
|
|
221
|
-
}
|
|
222
|
-
reject(e)
|
|
223
|
-
})
|
|
232
|
+
}, timeoutSeconds * 1000)
|
|
224
233
|
})
|
|
225
234
|
}
|
|
226
235
|
}
|