Skip to content

Commit 22e752b

Browse files
author
Matej Spiller Muys
committed
Replace locking fieldmap with concurrent safe haxmap
1 parent 5185ff8 commit 22e752b

File tree

7 files changed

+94
-233
lines changed

7 files changed

+94
-233
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
*.swp
33
*.swo
44
.idea
5+
*.iml
56
vendor
67
_test/test
78
_test/echo_server

field_map.go

+38-155
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ package quickfix
1717

1818
import (
1919
"bytes"
20-
"sort"
21-
"sync"
20+
"slices"
2221
"time"
22+
23+
"github.com/alphadose/haxmap"
2324
)
2425

2526
// field stores a slice of TagValues.
@@ -40,46 +41,33 @@ func writeField(f field, buffer *bytes.Buffer) {
4041
}
4142

4243
// tagOrder true if tag i should occur before tag j.
43-
type tagOrder func(i, j Tag) bool
44-
45-
type tagSort struct {
46-
tags []Tag
47-
compare tagOrder
48-
}
49-
50-
func (t tagSort) Len() int { return len(t.tags) }
51-
func (t tagSort) Swap(i, j int) { t.tags[i], t.tags[j] = t.tags[j], t.tags[i] }
52-
func (t tagSort) Less(i, j int) bool { return t.compare(t.tags[i], t.tags[j]) }
44+
type tagOrder func(i, j Tag) int
5345

5446
// FieldMap is a collection of fix fields that make up a fix message.
5547
type FieldMap struct {
56-
tagLookup map[Tag]field
57-
tagSort
58-
rwLock *sync.RWMutex
48+
tagLookup *haxmap.Map[Tag, field]
49+
compare tagOrder
5950
}
6051

6152
// ascending tags.
62-
func normalFieldOrder(i, j Tag) bool { return i < j }
53+
func normalFieldOrder(i, j Tag) int { return int(i - j) }
6354

6455
func (m *FieldMap) init() {
6556
m.initWithOrdering(normalFieldOrder)
6657
}
6758

6859
func (m *FieldMap) initWithOrdering(ordering tagOrder) {
69-
m.rwLock = &sync.RWMutex{}
70-
m.tagLookup = make(map[Tag]field)
60+
m.tagLookup = haxmap.New[Tag, field]()
7161
m.compare = ordering
7262
}
7363

7464
// Tags returns all of the Field Tags in this FieldMap.
7565
func (m FieldMap) Tags() []Tag {
76-
m.rwLock.RLock()
77-
defer m.rwLock.RUnlock()
78-
79-
tags := make([]Tag, 0, len(m.tagLookup))
80-
for t := range m.tagLookup {
81-
tags = append(tags, t)
82-
}
66+
var tags []Tag
67+
m.tagLookup.ForEach(func(tag Tag, _ field) bool {
68+
tags = append(tags, tag)
69+
return true
70+
})
8371

8472
return tags
8573
}
@@ -91,33 +79,13 @@ func (m FieldMap) Get(parser Field) MessageRejectError {
9179

9280
// Has returns true if the Tag is present in this FieldMap.
9381
func (m FieldMap) Has(tag Tag) bool {
94-
m.rwLock.RLock()
95-
defer m.rwLock.RUnlock()
96-
97-
_, ok := m.tagLookup[tag]
82+
_, ok := m.tagLookup.Get(tag)
9883
return ok
9984
}
10085

10186
// GetField parses of a field with Tag tag. Returned reject may indicate the field is not present, or the field value is invalid.
10287
func (m FieldMap) GetField(tag Tag, parser FieldValueReader) MessageRejectError {
103-
m.rwLock.RLock()
104-
defer m.rwLock.RUnlock()
105-
106-
f, ok := m.tagLookup[tag]
107-
if !ok {
108-
return ConditionallyRequiredFieldMissing(tag)
109-
}
110-
111-
if err := parser.Read(f[0].value); err != nil {
112-
return IncorrectDataFormatForValue(tag)
113-
}
114-
115-
return nil
116-
}
117-
118-
// GetField parses of a field with Tag tag. Returned reject may indicate the field is not present, or the field value is invalid.
119-
func (m FieldMap) getFieldNoLock(tag Tag, parser FieldValueReader) MessageRejectError {
120-
f, ok := m.tagLookup[tag]
88+
f, ok := m.tagLookup.Get(tag)
12189
if !ok {
12290
return ConditionallyRequiredFieldMissing(tag)
12391
}
@@ -131,20 +99,7 @@ func (m FieldMap) getFieldNoLock(tag Tag, parser FieldValueReader) MessageReject
13199

132100
// GetBytes is a zero-copy GetField wrapper for []bytes fields.
133101
func (m FieldMap) GetBytes(tag Tag) ([]byte, MessageRejectError) {
134-
m.rwLock.RLock()
135-
defer m.rwLock.RUnlock()
136-
137-
f, ok := m.tagLookup[tag]
138-
if !ok {
139-
return nil, ConditionallyRequiredFieldMissing(tag)
140-
}
141-
142-
return f[0].value, nil
143-
}
144-
145-
// getBytesNoLock is a lock free zero-copy GetField wrapper for []bytes fields.
146-
func (m FieldMap) getBytesNoLock(tag Tag) ([]byte, MessageRejectError) {
147-
f, ok := m.tagLookup[tag]
102+
f, ok := m.tagLookup.Get(tag)
148103
if !ok {
149104
return nil, ConditionallyRequiredFieldMissing(tag)
150105
}
@@ -176,26 +131,8 @@ func (m FieldMap) GetInt(tag Tag) (int, MessageRejectError) {
176131
return int(val), err
177132
}
178133

179-
// GetInt is a lock free GetField wrapper for int fields.
180-
func (m FieldMap) getIntNoLock(tag Tag) (int, MessageRejectError) {
181-
bytes, err := m.getBytesNoLock(tag)
182-
if err != nil {
183-
return 0, err
184-
}
185-
186-
var val FIXInt
187-
if val.Read(bytes) != nil {
188-
err = IncorrectDataFormatForValue(tag)
189-
}
190-
191-
return int(val), err
192-
}
193-
194134
// GetTime is a GetField wrapper for utc timestamp fields.
195135
func (m FieldMap) GetTime(tag Tag) (t time.Time, err MessageRejectError) {
196-
m.rwLock.RLock()
197-
defer m.rwLock.RUnlock()
198-
199136
bytes, err := m.GetBytes(tag)
200137
if err != nil {
201138
return
@@ -218,21 +155,9 @@ func (m FieldMap) GetString(tag Tag) (string, MessageRejectError) {
218155
return string(val), nil
219156
}
220157

221-
// GetString is a GetField wrapper for string fields.
222-
func (m FieldMap) getStringNoLock(tag Tag) (string, MessageRejectError) {
223-
var val FIXString
224-
if err := m.getFieldNoLock(tag, &val); err != nil {
225-
return "", err
226-
}
227-
return string(val), nil
228-
}
229-
230158
// GetGroup is a Get function specific to Group Fields.
231159
func (m FieldMap) GetGroup(parser FieldGroupReader) MessageRejectError {
232-
m.rwLock.RLock()
233-
defer m.rwLock.RUnlock()
234-
235-
f, ok := m.tagLookup[parser.Tag()]
160+
f, ok := m.tagLookup.Get(parser.Tag())
236161
if !ok {
237162
return ConditionallyRequiredFieldMissing(parser.Tag())
238163
}
@@ -277,67 +202,38 @@ func (m *FieldMap) SetString(tag Tag, value string) *FieldMap {
277202

278203
// Remove removes a tag from field map.
279204
func (m *FieldMap) Remove(tag Tag) {
280-
m.rwLock.Lock()
281-
defer m.rwLock.Unlock()
282-
283-
delete(m.tagLookup, tag)
205+
m.tagLookup.Del(tag)
284206
}
285207

286208
// Clear purges all fields from field map.
287209
func (m *FieldMap) Clear() {
288-
m.rwLock.Lock()
289-
defer m.rwLock.Unlock()
290-
291-
m.tags = m.tags[0:0]
292-
for k := range m.tagLookup {
293-
delete(m.tagLookup, k)
294-
}
295-
}
296-
297-
func (m *FieldMap) clearNoLock() {
298-
m.tags = m.tags[0:0]
299-
for k := range m.tagLookup {
300-
delete(m.tagLookup, k)
301-
}
210+
m.tagLookup.Clear()
302211
}
303212

304213
// CopyInto overwrites the given FieldMap with this one.
305214
func (m *FieldMap) CopyInto(to *FieldMap) {
306-
m.rwLock.RLock()
307-
defer m.rwLock.RUnlock()
308-
309-
to.tagLookup = make(map[Tag]field)
310-
for tag, f := range m.tagLookup {
215+
to.tagLookup = haxmap.New[Tag, field]()
216+
m.tagLookup.ForEach(func(tag Tag, f field) bool {
311217
clone := make(field, 1)
312218
clone[0] = f[0]
313-
to.tagLookup[tag] = clone
314-
}
315-
to.tags = make([]Tag, len(m.tags))
316-
copy(to.tags, m.tags)
219+
to.tagLookup.Set(tag, clone)
220+
return true
221+
})
317222
to.compare = m.compare
318223
}
319224

320225
func (m *FieldMap) add(f field) {
321-
t := fieldTag(f)
322-
if _, ok := m.tagLookup[t]; !ok {
323-
m.tags = append(m.tags, t)
324-
}
325-
326-
m.tagLookup[t] = f
226+
m.tagLookup.Set(fieldTag(f), f)
327227
}
328228

329229
func (m *FieldMap) getOrCreate(tag Tag) field {
330-
m.rwLock.Lock()
331-
defer m.rwLock.Unlock()
332-
333-
if f, ok := m.tagLookup[tag]; ok {
230+
if f, ok := m.tagLookup.Get(tag); ok {
334231
f = f[:1]
335232
return f
336233
}
337234

338235
f := make(field, 1)
339-
m.tagLookup[tag] = f
340-
m.tags = append(m.tags, tag)
236+
m.tagLookup.Set(tag, f)
341237
return f
342238
}
343239

@@ -350,65 +246,52 @@ func (m *FieldMap) Set(field FieldWriter) *FieldMap {
350246

351247
// SetGroup is a setter specific to group fields.
352248
func (m *FieldMap) SetGroup(field FieldGroupWriter) *FieldMap {
353-
m.rwLock.Lock()
354-
defer m.rwLock.Unlock()
355-
356-
_, ok := m.tagLookup[field.Tag()]
357-
if !ok {
358-
m.tags = append(m.tags, field.Tag())
359-
}
360-
m.tagLookup[field.Tag()] = field.Write()
249+
m.tagLookup.Set(field.Tag(), field.Write())
361250
return m
362251
}
363252

364253
func (m *FieldMap) sortedTags() []Tag {
365-
sort.Sort(m)
366-
return m.tags
254+
tags := m.Tags()
255+
slices.SortFunc(tags, m.compare)
256+
return tags
367257
}
368258

369259
func (m FieldMap) write(buffer *bytes.Buffer) {
370-
m.rwLock.Lock()
371-
defer m.rwLock.Unlock()
372-
373260
for _, tag := range m.sortedTags() {
374-
if f, ok := m.tagLookup[tag]; ok {
261+
if f, ok := m.tagLookup.Get(tag); ok {
375262
writeField(f, buffer)
376263
}
377264
}
378265
}
379266

380267
func (m FieldMap) total() int {
381-
m.rwLock.RLock()
382-
defer m.rwLock.RUnlock()
383-
384268
total := 0
385-
for _, fields := range m.tagLookup {
269+
m.tagLookup.ForEach(func(_ Tag, fields field) bool {
386270
for _, tv := range fields {
387271
switch tv.tag {
388272
case tagCheckSum: // Tag does not contribute to total.
389273
default:
390274
total += tv.total()
391275
}
392276
}
393-
}
277+
return true
278+
})
394279

395280
return total
396281
}
397282

398283
func (m FieldMap) length() int {
399-
m.rwLock.RLock()
400-
defer m.rwLock.RUnlock()
401-
402284
length := 0
403-
for _, fields := range m.tagLookup {
285+
m.tagLookup.ForEach(func(_ Tag, fields field) bool {
404286
for _, tv := range fields {
405287
switch tv.tag {
406288
case tagBeginString, tagBodyLength, tagCheckSum: // Tags do not contribute to length.
407289
default:
408290
length += tv.length()
409291
}
410292
}
411-
}
293+
return true
294+
})
412295

413296
return length
414297
}

go.mod

+11-9
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,32 @@ module github.com/quickfixgo/quickfix
33
go 1.21
44

55
require (
6+
github.com/alphadose/haxmap v1.4.1
67
github.com/mattn/go-sqlite3 v1.14.22
78
github.com/pires/go-proxyproto v0.7.0
89
github.com/pkg/errors v0.9.1
910
github.com/shopspring/decimal v1.4.0
10-
github.com/stretchr/testify v1.8.4
11-
go.mongodb.org/mongo-driver v1.15.0
12-
golang.org/x/net v0.24.0
11+
github.com/stretchr/testify v1.10.0
12+
go.mongodb.org/mongo-driver v1.17.1
13+
golang.org/x/net v0.33.0
1314
)
1415

1516
require (
1617
github.com/davecgh/go-spew v1.1.1 // indirect
1718
github.com/golang/snappy v0.0.4 // indirect
1819
github.com/klauspost/compress v1.15.12 // indirect
1920
github.com/kr/text v0.2.0 // indirect
20-
github.com/montanaflynn/stats v0.6.6 // indirect
21+
github.com/montanaflynn/stats v0.7.1 // indirect
2122
github.com/pmezard/go-difflib v1.0.0 // indirect
22-
github.com/stretchr/objx v0.5.0 // indirect
23+
github.com/stretchr/objx v0.5.2 // indirect
2324
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
2425
github.com/xdg-go/scram v1.1.2 // indirect
2526
github.com/xdg-go/stringprep v1.0.4 // indirect
26-
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
27-
golang.org/x/crypto v0.22.0 // indirect
28-
golang.org/x/sync v0.1.0 // indirect
29-
golang.org/x/text v0.14.0 // indirect
27+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
28+
golang.org/x/crypto v0.31.0 // indirect
29+
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 // indirect
30+
golang.org/x/sync v0.10.0 // indirect
31+
golang.org/x/text v0.21.0 // indirect
3032
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
3133
gopkg.in/yaml.v3 v3.0.1 // indirect
3234
)

0 commit comments

Comments
 (0)