jspurefix 5.11.0 → 5.11.2
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 +13 -1
- package/dist/buffer/fixml/fixml-parser.js +1 -1
- package/dist/buffer/fixml/fixml-parser.js.map +1 -1
- package/dist/config/empty-log-factory.d.ts +2 -2
- package/dist/config/empty-log-factory.js +1 -1
- package/dist/config/empty-log-factory.js.map +1 -1
- package/dist/config/get-js-fx-logger.d.ts +2 -2
- package/dist/config/get-js-fx-logger.js.map +1 -1
- package/dist/config/js-fix-logger-factory.d.ts +2 -2
- package/dist/config/js-fix-logger-factory.js.map +1 -1
- package/dist/config/js-fix-logger.d.ts +9 -8
- package/dist/config/js-fix-logger.js +4 -4
- package/dist/config/js-fix-logger.js.map +1 -1
- package/dist/config/js-fix-winston-log-factory.d.ts +2 -2
- package/dist/config/js-fix-winston-log-factory.js +2 -2
- package/dist/config/js-fix-winston-log-factory.js.map +1 -1
- package/dist/config/winston-logger.d.ts +5 -2
- package/dist/config/winston-logger.js +55 -9
- package/dist/config/winston-logger.js.map +1 -1
- package/dist/dictionary/fragment-safety.d.ts +26 -0
- package/dist/dictionary/fragment-safety.js +154 -0
- package/dist/dictionary/fragment-safety.js.map +1 -0
- package/dist/dictionary/index.d.ts +1 -0
- package/dist/dictionary/index.js +1 -0
- package/dist/dictionary/index.js.map +1 -1
- package/dist/runtime/make-config.js +1 -1
- package/dist/runtime/make-config.js.map +1 -1
- package/dist/runtime/session-launcher.js +1 -1
- package/dist/runtime/session-launcher.js.map +1 -1
- package/dist/sample/http/oms/http-client.js +1 -1
- package/dist/sample/http/oms/http-client.js.map +1 -1
- package/dist/sample/http/oms/http-server.js +1 -1
- package/dist/sample/http/oms/http-server.js.map +1 -1
- package/dist/sample/tcp/qf-md/md-client.js +1 -1
- package/dist/sample/tcp/qf-md/md-client.js.map +1 -1
- package/dist/sample/tcp/qf-md/md-server.js +1 -1
- package/dist/sample/tcp/qf-md/md-server.js.map +1 -1
- package/dist/sample/tcp/recovering-skeleton/respawn-acceptor.js +1 -1
- package/dist/sample/tcp/recovering-skeleton/respawn-acceptor.js.map +1 -1
- package/dist/sample/tcp/recovering-skeleton/skeleton-client.js +1 -1
- package/dist/sample/tcp/recovering-skeleton/skeleton-client.js.map +1 -1
- package/dist/sample/tcp/recovering-skeleton/skeleton-server.js +1 -1
- package/dist/sample/tcp/recovering-skeleton/skeleton-server.js.map +1 -1
- package/dist/sample/tcp/skeleton/skeleton-session.js +1 -1
- package/dist/sample/tcp/skeleton/skeleton-session.js.map +1 -1
- package/dist/sample/tcp/trade-capture/trade-capture-client.js +1 -1
- package/dist/sample/tcp/trade-capture/trade-capture-client.js.map +1 -1
- package/dist/sample/tcp/trade-capture/trade-capture-server.js +1 -1
- package/dist/sample/tcp/trade-capture/trade-capture-server.js.map +1 -1
- package/dist/store/fix-msg-memory-store.js +1 -1
- package/dist/store/fix-msg-memory-store.js.map +1 -1
- package/dist/transport/ascii/ascii-msg-transmitter.js +1 -1
- package/dist/transport/ascii/ascii-msg-transmitter.js.map +1 -1
- package/dist/transport/ascii/ascii-session.js +1 -1
- package/dist/transport/ascii/ascii-session.js.map +1 -1
- package/dist/transport/http/http-acceptor-listener.js +1 -1
- package/dist/transport/http/http-acceptor-listener.js.map +1 -1
- package/dist/transport/http/http-acceptor.js +2 -2
- package/dist/transport/http/http-acceptor.js.map +1 -1
- package/dist/transport/http/http-initiator.js +1 -1
- package/dist/transport/http/http-initiator.js.map +1 -1
- package/dist/transport/http/http-json-sample-adapter.js +1 -1
- package/dist/transport/http/http-json-sample-adapter.js.map +1 -1
- package/dist/transport/session/fix-session.js +3 -3
- package/dist/transport/session/fix-session.js.map +1 -1
- package/dist/transport/session/session-registry.js +1 -1
- package/dist/transport/session/session-registry.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.js +3 -2
- package/dist/transport/tcp/recovering-tcp-initiator.js.map +1 -1
- package/dist/transport/tcp/tcp-acceptor-listener.js +2 -2
- package/dist/transport/tcp/tcp-acceptor-listener.js.map +1 -1
- package/dist/transport/tcp/tcp-acceptor.js +2 -2
- package/dist/transport/tcp/tcp-acceptor.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator-connector.js +2 -2
- package/dist/transport/tcp/tcp-initiator-connector.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator.js +5 -4
- package/dist/transport/tcp/tcp-initiator.js.map +1 -1
- package/docs/instrumentation.md +346 -0
- package/docs/scattered-components.md +280 -0
- package/jsfix.test_client.txt +62 -62
- package/jsfix.test_server.txt +72 -72
- package/package.json +1 -1
- package/src/buffer/fixml/fixml-parser.ts +1 -1
- package/src/config/empty-log-factory.ts +2 -2
- package/src/config/get-js-fx-logger.ts +2 -2
- package/src/config/js-fix-logger-factory.ts +8 -2
- package/src/config/js-fix-logger.ts +23 -8
- package/src/config/js-fix-winston-log-factory.ts +3 -3
- package/src/config/winston-logger.ts +85 -10
- package/src/dictionary/fragment-safety.ts +225 -0
- package/src/dictionary/index.ts +1 -0
- package/src/runtime/make-config.ts +1 -1
- package/src/runtime/session-launcher.ts +1 -1
- package/src/sample/http/oms/http-client.ts +1 -1
- package/src/sample/http/oms/http-server.ts +1 -1
- package/src/sample/tcp/qf-md/md-client.ts +1 -1
- package/src/sample/tcp/qf-md/md-server.ts +1 -1
- package/src/sample/tcp/recovering-skeleton/respawn-acceptor.ts +2 -1
- package/src/sample/tcp/recovering-skeleton/skeleton-client.ts +1 -1
- package/src/sample/tcp/recovering-skeleton/skeleton-server.ts +1 -1
- package/src/sample/tcp/skeleton/skeleton-session.ts +1 -1
- package/src/sample/tcp/trade-capture/trade-capture-client.ts +2 -1
- package/src/sample/tcp/trade-capture/trade-capture-server.ts +2 -1
- package/src/store/fix-msg-memory-store.ts +2 -1
- package/src/transport/ascii/ascii-msg-transmitter.ts +2 -1
- package/src/transport/ascii/ascii-session.ts +2 -1
- package/src/transport/http/http-acceptor-listener.ts +2 -1
- package/src/transport/http/http-acceptor.ts +2 -1
- package/src/transport/http/http-initiator.ts +2 -1
- package/src/transport/http/http-json-sample-adapter.ts +1 -1
- package/src/transport/session/fix-session.ts +2 -1
- package/src/transport/session/session-registry.ts +1 -1
- package/src/transport/tcp/connect-error.ts +56 -0
- package/src/transport/tcp/recovering-tcp-initiator.ts +4 -2
- package/src/transport/tcp/tcp-acceptor-listener.ts +4 -2
- package/src/transport/tcp/tcp-acceptor.ts +2 -1
- package/src/transport/tcp/tcp-initiator-connector.ts +4 -2
- package/src/transport/tcp/tcp-initiator.ts +9 -5
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContainedComponentField,
|
|
3
|
+
ContainedField,
|
|
4
|
+
ContainedFieldType,
|
|
5
|
+
ContainedGroupField,
|
|
6
|
+
ContainedSimpleField,
|
|
7
|
+
IContainedSet
|
|
8
|
+
} from './contained'
|
|
9
|
+
import { FixDefinitions } from './definition'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Static analysis of a dictionary, asking one question: if a component's tags arrived
|
|
13
|
+
* scattered through a message rather than adjacently, could this dictionary still say
|
|
14
|
+
* unambiguously which component each tag belongs to?
|
|
15
|
+
*
|
|
16
|
+
* The segment parser normally answers that from *position* - a tag between the first and
|
|
17
|
+
* last tag of a component belongs to it. A counterparty that emits a component's tags
|
|
18
|
+
* non-adjacently breaks the inference, and the tags themselves become the only evidence
|
|
19
|
+
* left. That works whenever the children of one set own disjoint tags, which FIX's tag
|
|
20
|
+
* numbering conventions (Underlying*, Leg*, NestedParties2/3) exist to guarantee, but a
|
|
21
|
+
* house dictionary is under no obligation to honour them.
|
|
22
|
+
*
|
|
23
|
+
* See docs/scattered-components.md. Nothing in the engine consumes this yet - it is a
|
|
24
|
+
* diagnostic run against a dictionary to find out whether the assumption holds before any
|
|
25
|
+
* code is written that depends on it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export enum FragmentSafetyCode {
|
|
29
|
+
/**
|
|
30
|
+
* two children of one set both claim the same tag, so a scattered instance of either
|
|
31
|
+
* could not be attributed to one of them by tag alone
|
|
32
|
+
*/
|
|
33
|
+
SiblingTagCollision = 'sibling-tag-collision',
|
|
34
|
+
/**
|
|
35
|
+
* a component owns no tag at its own level, so a scattered instance leaves no evidence
|
|
36
|
+
* it was ever present
|
|
37
|
+
*/
|
|
38
|
+
UnclaimableComponent = 'unclaimable-component'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface IFragmentSafetyFinding {
|
|
42
|
+
readonly code: FragmentSafetyCode
|
|
43
|
+
/**
|
|
44
|
+
* dotted path to the set the finding is about, from the message it was first reached
|
|
45
|
+
* through e.g. 'NewOrderSingle.Instrument'
|
|
46
|
+
*/
|
|
47
|
+
readonly path: string
|
|
48
|
+
/**
|
|
49
|
+
* name of the set whose children collide, or of the unclaimable component
|
|
50
|
+
*/
|
|
51
|
+
readonly set: string
|
|
52
|
+
/**
|
|
53
|
+
* the contended tag, or 0 for an unclaimable component
|
|
54
|
+
*/
|
|
55
|
+
readonly tag: number
|
|
56
|
+
/**
|
|
57
|
+
* names of the children of `set` that each claim `tag`
|
|
58
|
+
*/
|
|
59
|
+
readonly owners: string[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function describeFinding (f: IFragmentSafetyFinding): string {
|
|
63
|
+
switch (f.code) {
|
|
64
|
+
case FragmentSafetyCode.SiblingTagCollision:
|
|
65
|
+
return `${f.path}: tag ${f.tag} is claimed by ${f.owners.join(' and ')}`
|
|
66
|
+
case FragmentSafetyCode.UnclaimableComponent:
|
|
67
|
+
return `${f.path}: owns no tag of its own`
|
|
68
|
+
default:
|
|
69
|
+
return f.path
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class FragmentSafety {
|
|
74
|
+
private readonly findings: IFragmentSafetyFinding[] = []
|
|
75
|
+
private readonly analysed = new Set<IContainedSet>()
|
|
76
|
+
private readonly owned = new Map<IContainedSet, number[]>()
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* tags a set can claim at its own level - its simple fields, the same from any
|
|
80
|
+
* component nested below it, and the NumInGroup tag of any group it contains.
|
|
81
|
+
*
|
|
82
|
+
* a group's *interior* is deliberately excluded. a repeating group is the one
|
|
83
|
+
* self describing construct in tag value FIX, so its instances are always delimiter
|
|
84
|
+
* framed and contiguous, and its member tags are never evidence about the level above.
|
|
85
|
+
* this is the same distinction `flattenedTag` does not draw.
|
|
86
|
+
*/
|
|
87
|
+
public ownTags (set: IContainedSet): number[] {
|
|
88
|
+
const memo = this.owned.get(set)
|
|
89
|
+
if (memo) return memo
|
|
90
|
+
// seed before recursing so a self referencing dictionary terminates
|
|
91
|
+
this.owned.set(set, [])
|
|
92
|
+
const tags: number[] = []
|
|
93
|
+
for (const field of set.fields) {
|
|
94
|
+
switch (field.type) {
|
|
95
|
+
case ContainedFieldType.Simple: {
|
|
96
|
+
tags.push((field as ContainedSimpleField).definition.tag)
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
case ContainedFieldType.Component: {
|
|
100
|
+
const cf = field as ContainedComponentField
|
|
101
|
+
for (const t of this.ownTags(cf.definition)) {
|
|
102
|
+
tags.push(t)
|
|
103
|
+
}
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
case ContainedFieldType.Group: {
|
|
107
|
+
const gf = field as ContainedGroupField
|
|
108
|
+
const nof = gf.definition.noOfField
|
|
109
|
+
if (nof) {
|
|
110
|
+
tags.push(nof.tag)
|
|
111
|
+
}
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
default:
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.owned.set(set, tags)
|
|
119
|
+
return tags
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* every message in the dictionary, and every component and group reachable from one,
|
|
124
|
+
* each analysed once however many messages reach it
|
|
125
|
+
*/
|
|
126
|
+
public static analyse (definitions: FixDefinitions): IFragmentSafetyFinding[] {
|
|
127
|
+
const instance = new FragmentSafety()
|
|
128
|
+
// the message map is keyed by name, msgType and abbreviation, so walk distinct sets
|
|
129
|
+
const seen = new Set<IContainedSet>()
|
|
130
|
+
for (const message of definitions.message.values()) {
|
|
131
|
+
if (seen.has(message)) continue
|
|
132
|
+
seen.add(message)
|
|
133
|
+
instance.walk(message, message.name)
|
|
134
|
+
}
|
|
135
|
+
return instance.findings
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* a single set and everything below it, for a dictionary fragment under test
|
|
140
|
+
*/
|
|
141
|
+
public static analyseSet (set: IContainedSet, path?: string): IFragmentSafetyFinding[] {
|
|
142
|
+
const instance = new FragmentSafety()
|
|
143
|
+
instance.walk(set, path ?? set.name)
|
|
144
|
+
return instance.findings
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private walk (set: IContainedSet, path: string): void {
|
|
148
|
+
if (this.analysed.has(set)) return
|
|
149
|
+
this.analysed.add(set)
|
|
150
|
+
this.checkSiblings(set, path)
|
|
151
|
+
for (const field of set.fields) {
|
|
152
|
+
switch (field.type) {
|
|
153
|
+
case ContainedFieldType.Component: {
|
|
154
|
+
const cf = field as ContainedComponentField
|
|
155
|
+
this.checkClaimable(cf, `${path}.${cf.name}`)
|
|
156
|
+
this.walk(cf.definition, `${path}.${cf.name}`)
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
case ContainedFieldType.Group: {
|
|
160
|
+
// the instance body of a group is a level in its own right, and everything
|
|
161
|
+
// said about scattering above applies again inside it
|
|
162
|
+
const gf = field as ContainedGroupField
|
|
163
|
+
this.walk(gf.definition, `${path}.${gf.name}`)
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
break
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private checkClaimable (cf: ContainedComponentField, path: string): void {
|
|
173
|
+
if (this.ownTags(cf.definition).length > 0) return
|
|
174
|
+
this.findings.push({
|
|
175
|
+
code: FragmentSafetyCode.UnclaimableComponent,
|
|
176
|
+
path,
|
|
177
|
+
set: cf.name,
|
|
178
|
+
tag: 0,
|
|
179
|
+
owners: []
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private checkSiblings (set: IContainedSet, path: string): void {
|
|
184
|
+
// tag -> the children of this set claiming it. a tag reaching two children is what
|
|
185
|
+
// makes a scattered instance unattributable
|
|
186
|
+
const claims = new Map<number, string[]>()
|
|
187
|
+
for (const field of set.fields) {
|
|
188
|
+
for (const tag of this.claimsOf(field)) {
|
|
189
|
+
const owners = claims.get(tag)
|
|
190
|
+
if (owners) {
|
|
191
|
+
if (!owners.includes(field.name)) {
|
|
192
|
+
owners.push(field.name)
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
claims.set(tag, [field.name])
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const [tag, owners] of claims) {
|
|
200
|
+
if (owners.length < 2) continue
|
|
201
|
+
this.findings.push({
|
|
202
|
+
code: FragmentSafetyCode.SiblingTagCollision,
|
|
203
|
+
path,
|
|
204
|
+
set: set.name,
|
|
205
|
+
tag,
|
|
206
|
+
owners
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private claimsOf (field: ContainedField): number[] {
|
|
212
|
+
switch (field.type) {
|
|
213
|
+
case ContainedFieldType.Simple:
|
|
214
|
+
return [(field as ContainedSimpleField).definition.tag]
|
|
215
|
+
case ContainedFieldType.Component:
|
|
216
|
+
return this.ownTags((field as ContainedComponentField).definition)
|
|
217
|
+
case ContainedFieldType.Group: {
|
|
218
|
+
const nof = (field as ContainedGroupField).definition.noOfField
|
|
219
|
+
return nof ? [nof.tag] : []
|
|
220
|
+
}
|
|
221
|
+
default:
|
|
222
|
+
return []
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
package/src/dictionary/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ export class RuntimeFactory {
|
|
|
25
25
|
if (!type) reject(new Error('no application type in application object'))
|
|
26
26
|
this.definitionFactory.getDefinitions(path,
|
|
27
27
|
(t: string) => {
|
|
28
|
-
return this.logFactory.logger(`${type}.${t}
|
|
28
|
+
return this.logFactory.logger(`${type}.${t}`, { component: t, role: type })
|
|
29
29
|
}).then((definitions: FixDefinitions) => {
|
|
30
30
|
const config = new JsFixConfig(this.msgFactory, definitions, description, AsciiChars.Soh, this.logFactory)
|
|
31
31
|
resolve(config)
|
|
@@ -19,7 +19,7 @@ export abstract class SessionLauncher {
|
|
|
19
19
|
acceptorConfig: string | ISessionDescription | null = null,
|
|
20
20
|
private readonly loggerFactory: JsFixLoggerFactory = defaultLoggerFactory
|
|
21
21
|
) {
|
|
22
|
-
this.logger = this.loggerFactory.logger('launcher')
|
|
22
|
+
this.logger = this.loggerFactory.logger('launcher', { component: 'SessionLauncher' })
|
|
23
23
|
this.initiatorConfig = initiatorConfig ? this.loadConfig(initiatorConfig) : null
|
|
24
24
|
this.acceptorConfig = acceptorConfig ? this.loadConfig(acceptorConfig) : null
|
|
25
25
|
}
|
|
@@ -16,7 +16,7 @@ export class HttpClient extends FixmlSession {
|
|
|
16
16
|
super(config)
|
|
17
17
|
this.logReceivedMsgs = true
|
|
18
18
|
this.fixLog = config.logFactory.plain(`jsfix.${config?.description?.application?.name}.txt`)
|
|
19
|
-
this.logger = config.logFactory.logger(`${this.me}
|
|
19
|
+
this.logger = config.logFactory.logger(`${this.me}`, { component: 'HttpClient', app: this.me })
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
protected onApplicationMsg (msgType: string, view: MsgView): void {
|
|
@@ -16,7 +16,7 @@ export class HttpServer extends FixmlSession {
|
|
|
16
16
|
super(config)
|
|
17
17
|
this.logReceivedMsgs = true
|
|
18
18
|
this.fixLog = config.logFactory.plain(`jsfix.${config?.description?.application?.name}.txt`)
|
|
19
|
-
this.logger = config.logFactory.logger(`${this.me}
|
|
19
|
+
this.logger = config.logFactory.logger(`${this.me}`, { component: 'HttpServer', app: this.me })
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
protected onApplicationMsg (msgType: string, view: MsgView): void {
|
|
@@ -16,7 +16,7 @@ export class MDClient extends AsciiSession {
|
|
|
16
16
|
super(config)
|
|
17
17
|
this.logReceivedMsgs = true
|
|
18
18
|
this.fixLog = config.logFactory.plain(`jsfix.${config?.description?.application?.name}.txt`)
|
|
19
|
-
this.logger = config.logFactory.logger(`${this.me}:MDClient
|
|
19
|
+
this.logger = config.logFactory.logger(`${this.me}:MDClient`, { component: 'MDClient', app: this.me })
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
protected onApplicationMsg (_: string, view: MsgView): void {
|
|
@@ -16,7 +16,7 @@ export class MDServer extends AsciiSession {
|
|
|
16
16
|
constructor (@inject('IJsFixConfig') public readonly config: IJsFixConfig) {
|
|
17
17
|
super(config)
|
|
18
18
|
this.logReceivedMsgs = true
|
|
19
|
-
this.logger = config.logFactory.logger(`${this.me}:MDServer
|
|
19
|
+
this.logger = config.logFactory.logger(`${this.me}:MDServer`, { component: 'MDServer', app: this.me })
|
|
20
20
|
this.fixLog = config.logFactory.plain(`jsfix.${config?.description?.application?.name}.txt`)
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -13,7 +13,8 @@ export class RespawnAcceptor extends FixEntity {
|
|
|
13
13
|
|
|
14
14
|
constructor (@inject('IJsFixConfig') public readonly config: IJsFixConfig) {
|
|
15
15
|
super(config)
|
|
16
|
-
this.logger = config.logFactory.logger('RespawnAcceptor'
|
|
16
|
+
this.logger = config.logFactory.logger('RespawnAcceptor',
|
|
17
|
+
{ component: 'RespawnAcceptor', role: 'acceptor' })
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
// if acceptor errors e.g. via a forced connection drop, then respawn
|
|
@@ -13,7 +13,7 @@ export class SkeletonClient extends AsciiSession {
|
|
|
13
13
|
super(config)
|
|
14
14
|
this.logReceivedMsgs = true
|
|
15
15
|
this.fixLog = config.logFactory.plain(`jsfix.${config?.description?.application?.name}.txt`)
|
|
16
|
-
this.logger = config.logFactory.logger(`${this.me}
|
|
16
|
+
this.logger = config.logFactory.logger(`${this.me}`, { component: 'SkeletonClient', app: this.me })
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
protected onApplicationMsg (msgType: string, view: MsgView): void {
|
|
@@ -13,7 +13,7 @@ export class SkeletonServer extends AsciiSession {
|
|
|
13
13
|
super(config)
|
|
14
14
|
this.logReceivedMsgs = true
|
|
15
15
|
this.fixLog = config.logFactory.plain(`jsfix.${config?.description?.application?.name}.txt`)
|
|
16
|
-
this.logger = config.logFactory.logger(`${this.me}
|
|
16
|
+
this.logger = config.logFactory.logger(`${this.me}`, { component: 'SkeletonServer', app: this.me })
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
protected onApplicationMsg (msgType: string, _: MsgView): void {
|
|
@@ -17,7 +17,7 @@ export class SkeletonSession extends AsciiSession {
|
|
|
17
17
|
super(config)
|
|
18
18
|
this.logReceivedMsgs = true
|
|
19
19
|
this.fixLog = config.logFactory.plain(`jsfix.${config?.description?.application?.name}.txt`)
|
|
20
|
-
this.logger = config.logFactory.logger(`${this.me}
|
|
20
|
+
this.logger = config.logFactory.logger(`${this.me}`, { component: 'SkeletonSession', app: this.me })
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
protected onApplicationMsg (msgType: string, view: MsgView): void {
|
|
@@ -18,7 +18,8 @@ export class TradeCaptureClient extends AsciiSession {
|
|
|
18
18
|
this.logReceivedMsgs = true
|
|
19
19
|
this.reports = new Map<string, ITradeCaptureReport>()
|
|
20
20
|
this.fixLog = config.logFactory.plain(`jsfix.${config?.description?.application?.name}.txt`)
|
|
21
|
-
this.logger = config.logFactory.logger(`${this.me}:TradeCaptureClient
|
|
21
|
+
this.logger = config.logFactory.logger(`${this.me}:TradeCaptureClient`,
|
|
22
|
+
{ component: 'TradeCaptureClient', app: this.me })
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
protected onApplicationMsg (msgType: string, view: MsgView): void {
|
|
@@ -18,7 +18,8 @@ export class TradeCaptureServer extends AsciiSession {
|
|
|
18
18
|
constructor (public readonly config: IJsFixConfig) {
|
|
19
19
|
super(config)
|
|
20
20
|
this.logReceivedMsgs = true
|
|
21
|
-
this.logger = config.logFactory.logger(`${this.me}:TradeCaptureServer
|
|
21
|
+
this.logger = config.logFactory.logger(`${this.me}:TradeCaptureServer`,
|
|
22
|
+
{ component: 'TradeCaptureServer', app: this.me })
|
|
22
23
|
this.fixLog = config.logFactory.plain(`jsfix.${config?.description?.application?.name}.txt`)
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -20,7 +20,8 @@ export class FixMsgMemoryStore implements IFixMsgStore {
|
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
public constructor (public readonly id: string, public readonly config: IJsFixConfig) {
|
|
23
|
-
this.logger = config.logFactory.logger(`${this.id}:FixMsgMemoryStore
|
|
23
|
+
this.logger = config.logFactory.logger(`${this.id}:FixMsgMemoryStore`,
|
|
24
|
+
{ component: 'FixMsgMemoryStore', store: this.id })
|
|
24
25
|
this.setExcMsgType([])
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -39,7 +39,8 @@ export class AsciiMsgTransmitter extends MsgTransmitter {
|
|
|
39
39
|
*/
|
|
40
40
|
private reportUnknownFields (config: IJsFixConfig): void {
|
|
41
41
|
const name = config.description.application?.name ?? 'na'
|
|
42
|
-
const logger = config.logFactory.logger(`${name}:encoder
|
|
42
|
+
const logger = config.logFactory.logger(`${name}:encoder`,
|
|
43
|
+
{ component: 'AsciiMsgTransmitter', app: name })
|
|
43
44
|
const reported = new Set<string>()
|
|
44
45
|
this.encoder.onUnknownField = (fieldName: string, setName: string) => {
|
|
45
46
|
const key = `${setName}.${fieldName}`
|
|
@@ -662,7 +662,8 @@ export abstract class AsciiSession extends FixSession {
|
|
|
662
662
|
|
|
663
663
|
// every session on a wildcard listener shared one logger name until now, which
|
|
664
664
|
// makes a multi-client server log impossible to follow
|
|
665
|
-
this.sessionLogger = this.config.logFactory.logger(`${this.me}:${peerCompId}:FixSession
|
|
665
|
+
this.sessionLogger = this.config.logFactory.logger(`${this.me}:${peerCompId}:FixSession`,
|
|
666
|
+
{ component: 'FixSession', app: this.me, peer: peerCompId })
|
|
666
667
|
|
|
667
668
|
this.sessionId = new SessionId(
|
|
668
669
|
this.config.description.BeginString,
|
|
@@ -15,7 +15,8 @@ export class HttpAcceptorListener extends FixEntity {
|
|
|
15
15
|
|
|
16
16
|
async start (): Promise<any> {
|
|
17
17
|
return new Promise<any>(async (resolve, reject) => {
|
|
18
|
-
const logger = this.config.logFactory.logger('acceptor'
|
|
18
|
+
const logger = this.config.logFactory.logger('acceptor',
|
|
19
|
+
{ component: 'HttpAcceptorListener', role: 'acceptor' })
|
|
19
20
|
const sessionContainer = this.config.sessionContainer
|
|
20
21
|
if (!sessionContainer.isRegistered(DITokens.FixSession)) {
|
|
21
22
|
reject(new Error(`application must register a DI token '${DITokens.FixSession}' - see src/sample`))
|
|
@@ -22,7 +22,8 @@ export class HttpAcceptor extends FixAcceptor {
|
|
|
22
22
|
|
|
23
23
|
constructor (@inject(DITokens.IJsFixConfig) public readonly config: IJsFixConfig) {
|
|
24
24
|
super(config?.description?.application ?? null)
|
|
25
|
-
this.logger = config.logFactory.logger(`${config?.description?.application?.name}:HttpAcceptor
|
|
25
|
+
this.logger = config.logFactory.logger(`${config?.description?.application?.name}:HttpAcceptor`,
|
|
26
|
+
{ component: 'HttpAcceptor', app: config?.description?.application?.name, role: 'acceptor' })
|
|
26
27
|
this.logger.info('creating http server')
|
|
27
28
|
this.router = express.Router()
|
|
28
29
|
this.router.use(bodyParser.json())
|
|
@@ -11,7 +11,8 @@ export class HttpInitiator extends FixEntity {
|
|
|
11
11
|
logger: IJsFixLogger
|
|
12
12
|
constructor (@inject(DITokens.IJsFixConfig) public readonly config: IJsFixConfig) {
|
|
13
13
|
super(config)
|
|
14
|
-
this.logger = config.logFactory.logger('initiator'
|
|
14
|
+
this.logger = config.logFactory.logger('initiator',
|
|
15
|
+
{ component: 'HttpInitiator', role: 'initiator' })
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
async start (): Promise<any> {
|
|
@@ -13,7 +13,7 @@ export class HttpJsonSampleAdapter implements IHttpAdapter {
|
|
|
13
13
|
private token: string | null = null
|
|
14
14
|
private readonly routes: Map<string, IHtmlRoute> = new Map<string, IHtmlRoute>()
|
|
15
15
|
constructor (@inject(DITokens.IJsFixConfig) public readonly config: IJsFixConfig) {
|
|
16
|
-
this.logger = config.logFactory.logger('http.adapter')
|
|
16
|
+
this.logger = config.logFactory.logger('http.adapter', { component: 'HttpJsonSampleAdapter' })
|
|
17
17
|
const routes = this.routes
|
|
18
18
|
const options = config?.description?.application?.http?.options
|
|
19
19
|
if (!options) {
|
|
@@ -61,7 +61,8 @@ export abstract class FixSession extends events.EventEmitter {
|
|
|
61
61
|
heartBeat: config.description.HeartBtInt,
|
|
62
62
|
lastPeerMsgSeqNum: config.description.LastReceivedSeqNum
|
|
63
63
|
})
|
|
64
|
-
this.sessionLogger = config.logFactory.logger(`${this.me}:FixSession
|
|
64
|
+
this.sessionLogger = config.logFactory.logger(`${this.me}:FixSession`,
|
|
65
|
+
{ component: 'FixSession', app: this.me, role: description?.application?.type })
|
|
65
66
|
this.initiator = description?.application?.type === 'initiator'
|
|
66
67
|
this.acceptor = !this.initiator
|
|
67
68
|
this.checkMsgIntegrity = this.acceptor
|
|
@@ -53,7 +53,7 @@ export class SessionRegistry implements ISessionRegistry {
|
|
|
53
53
|
private readonly logger: IJsFixLogger | null
|
|
54
54
|
|
|
55
55
|
constructor (logFactory?: JsFixLoggerFactory) {
|
|
56
|
-
this.logger = logFactory?.logger('SessionRegistry') ?? null
|
|
56
|
+
this.logger = logFactory?.logger('SessionRegistry', { component: 'SessionRegistry' }) ?? null
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
public get count (): number {
|
|
@@ -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
|
|
@@ -38,7 +39,8 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
38
39
|
super(jsFixConfig)
|
|
39
40
|
this.application = this.jsFixConfig.description.application ?? null
|
|
40
41
|
const name = this.application?.name ?? 'na'
|
|
41
|
-
this.logger = jsFixConfig.logFactory.logger(`${name}:RecoveringTcpInitiator
|
|
42
|
+
this.logger = jsFixConfig.logFactory.logger(`${name}:RecoveringTcpInitiator`,
|
|
43
|
+
{ component: 'RecoveringTcpInitiator', app: name, role: 'initiator' })
|
|
42
44
|
if (!this.application) {
|
|
43
45
|
throw new Error('no application in session description.')
|
|
44
46
|
}
|
|
@@ -163,7 +165,7 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
163
165
|
this.connect(this.connectTimeoutSecs).then(t => {
|
|
164
166
|
this.logger.info(`new transport ${t.id}`)
|
|
165
167
|
}).catch((e) => {
|
|
166
|
-
this.logger.info(`failed to re-connect ${e
|
|
168
|
+
this.logger.info(`failed to re-connect: ${describeConnectError(e)} - backoff for ${this.backoffFailConnectSecs}`)
|
|
167
169
|
this.th = setTimeout(() => {
|
|
168
170
|
this.logger.info('returning to recover()')
|
|
169
171
|
setImmediate(() => {
|
|
@@ -27,7 +27,8 @@ export class TcpAcceptorListener extends FixEntity {
|
|
|
27
27
|
|
|
28
28
|
async start (): Promise<any> {
|
|
29
29
|
return await new Promise<any>(async (resolve, reject) => {
|
|
30
|
-
const logger = this.config.logFactory.logger('acceptor'
|
|
30
|
+
const logger = this.config.logFactory.logger('acceptor',
|
|
31
|
+
{ component: 'TcpAcceptorListener', role: 'acceptor' })
|
|
31
32
|
const sessionContainer = this.config.sessionContainer
|
|
32
33
|
if (!sessionContainer.isRegistered(DITokens.FixSession)) {
|
|
33
34
|
reject(new Error(`application must register a DI token '${DITokens.FixSession}' - see src/sample`))
|
|
@@ -93,7 +94,8 @@ export class TcpAcceptorListener extends FixEntity {
|
|
|
93
94
|
*/
|
|
94
95
|
stop (): void {
|
|
95
96
|
if (this.acceptor) {
|
|
96
|
-
const logger = this.config.logFactory.logger('acceptor'
|
|
97
|
+
const logger = this.config.logFactory.logger('acceptor',
|
|
98
|
+
{ component: 'TcpAcceptorListener', role: 'acceptor' })
|
|
97
99
|
logger.info('acceptor stopping')
|
|
98
100
|
this.acceptor.close(() => {
|
|
99
101
|
logger.info('acceptor closed.')
|
|
@@ -18,7 +18,8 @@ export class TcpAcceptor extends FixAcceptor {
|
|
|
18
18
|
|
|
19
19
|
constructor (@inject(DITokens.IJsFixConfig) public readonly config: IJsFixConfig) {
|
|
20
20
|
super(config.description.application ?? null)
|
|
21
|
-
this.logger = config.logFactory.logger(`${config.description.application?.name}:TcpAcceptor
|
|
21
|
+
this.logger = config.logFactory.logger(`${config.description.application?.name}:TcpAcceptor`,
|
|
22
|
+
{ component: 'TcpAcceptor', app: config.description.application?.name, role: 'acceptor' })
|
|
22
23
|
const tlsOptions: TlsOptions | null = this.tlsOptions()
|
|
23
24
|
if (tlsOptions) {
|
|
24
25
|
this.tlsServer()
|
|
@@ -21,7 +21,8 @@ export class TcpInitiatorConnector extends FixEntity {
|
|
|
21
21
|
async start (reconnectTimeout: number = 0): Promise<any> {
|
|
22
22
|
|
|
23
23
|
return await new Promise<any>(async (resolve, reject) => {
|
|
24
|
-
const logger = this.config.logFactory.logger('initiator'
|
|
24
|
+
const logger = this.config.logFactory.logger('initiator',
|
|
25
|
+
{ component: 'TcpInitiatorConnector', role: 'initiator' })
|
|
25
26
|
const sessionContainer = this.config.sessionContainer
|
|
26
27
|
if (!sessionContainer.isRegistered(DITokens.FixSession)) {
|
|
27
28
|
reject(new Error(`application must register a DI token '${DITokens.FixSession}' - see src/sample`))
|
|
@@ -64,7 +65,8 @@ export class TcpInitiatorConnector extends FixEntity {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
async connect (initiatorSession: FixSession): Promise<any> {
|
|
67
|
-
const logger = this.config.logFactory.logger('initiator'
|
|
68
|
+
const logger = this.config.logFactory.logger('initiator',
|
|
69
|
+
{ component: 'TcpInitiatorConnector', role: 'initiator' })
|
|
68
70
|
const initiator: FixInitiator = this.config.sessionContainer.resolve<FixInitiator>(TcpInitiator)
|
|
69
71
|
logger.info('connecting ...')
|
|
70
72
|
const initiatorTransport: MsgTransport = await initiator.connect(this.connectTimeoutSecs)
|
|
@@ -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'
|
|
@@ -33,7 +33,8 @@ export class TcpInitiator extends FixInitiator {
|
|
|
33
33
|
constructor (@inject(DITokens.IJsFixConfig) public readonly jsFixConfig: IJsFixConfig) {
|
|
34
34
|
super(jsFixConfig.description.application ?? null)
|
|
35
35
|
const name = this.application?.name ?? 'initiator'
|
|
36
|
-
this.logger = jsFixConfig.logFactory.logger(`${name}:TcpInitiator
|
|
36
|
+
this.logger = jsFixConfig.logFactory.logger(`${name}:TcpInitiator`,
|
|
37
|
+
{ component: 'TcpInitiator', app: name, role: 'initiator' })
|
|
37
38
|
if (!this.application) {
|
|
38
39
|
throw new Error('no application in session description.')
|
|
39
40
|
}
|
|
@@ -74,7 +75,7 @@ export class TcpInitiator extends FixInitiator {
|
|
|
74
75
|
// connect() whose timeout expires before the first retry interval fires - the
|
|
75
76
|
// default case, timeout and reconnectSeconds are both 5 - reported a generic
|
|
76
77
|
// "timeout whilst connecting" and threw away the reason. see #94.
|
|
77
|
-
this.logger.info(`first connect attempt failed: ${first
|
|
78
|
+
this.logger.info(`first connect attempt failed: ${describeConnectError(first)}`)
|
|
78
79
|
this.repeatConnect(timeoutSeconds, first)
|
|
79
80
|
.then((t: MsgTransport) => { resolve(t) })
|
|
80
81
|
.catch((e: Error) => { reject(e) })
|
|
@@ -212,7 +213,7 @@ export class TcpInitiator extends FixInitiator {
|
|
|
212
213
|
resolve(t)
|
|
213
214
|
}).catch((e: Error) => {
|
|
214
215
|
lastError = e
|
|
215
|
-
this.logger.info(`${name}: retries ${retries} ${e
|
|
216
|
+
this.logger.info(`${name}: retries ${retries} ${describeConnectError(e)}`)
|
|
216
217
|
})
|
|
217
218
|
}, reconnectSeconds * 1000)
|
|
218
219
|
// a plain timer, not a promisified one: promisify(setTimeout) hands back no
|
|
@@ -223,7 +224,10 @@ export class TcpInitiator extends FixInitiator {
|
|
|
223
224
|
this.timeoutTh = setTimeout(() => {
|
|
224
225
|
this.clearTimer()
|
|
225
226
|
this.state = InitiatorState.Stopped
|
|
226
|
-
|
|
227
|
+
// this error is what the application catches from run() - so give it a message
|
|
228
|
+
// it can print. An AggregateError arrives with none at all, which meant a
|
|
229
|
+
// caller handling a failed connect had nothing to report but an empty string.
|
|
230
|
+
const e = withConnectReason(lastError ?? new Error(`${name}: timeout of ${timeoutSeconds} whilst connecting`))
|
|
227
231
|
this.logger.warning(`${name}: giving up after ${timeoutSeconds}s and ${retries} retries, last error: ${e.message}`)
|
|
228
232
|
reject(e)
|
|
229
233
|
}, timeoutSeconds * 1000)
|