diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml new file mode 100644 index 000000000..c634318df --- /dev/null +++ b/.github/workflows/golangci-lint.yaml @@ -0,0 +1,28 @@ +name: golangci-lint +on: + push: + tags: + - v* + branches: + - master + pull_request: +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v1 + with: + # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. + version: v1.26 + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # args: --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index c6137fd7a..d5b3e32c3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ _test/echo_server _test/tmp _vendor* gen +*.db +.idea \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index b6f063217..5849b2f95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,43 @@ language: go sudo: false +services: + - mongodb + go: - - 1.9 + - 1.11.x + - 1.12.x + - 1.13 - tip env: - - FIX_TEST= - - FIX_TEST=fix40 - - FIX_TEST=fix41 - - FIX_TEST=fix42 - - FIX_TEST=fix43 - - FIX_TEST=fix44 - - FIX_TEST=fix50 - - FIX_TEST=fix50sp1 - - FIX_TEST=fix50sp2 + global: + - GO111MODULE=on + - MONGODB_TEST_CXN=localhost + matrix: + - FIX_TEST= + - FIX_TEST=fix40 + - FIX_TEST=fix41 + - FIX_TEST=fix42 + - FIX_TEST=fix43 + - FIX_TEST=fix44 + - FIX_TEST=fix50 + - FIX_TEST=fix50sp1 + - FIX_TEST=fix50sp2 matrix: allow_failures: - go: tip + fast_finish: true + exclude: + - go: 1.13.x + env: GO111MODULE=on + - go: tip + env: GO111MODULE=on -install: - - go get -u github.com/golang/dep/cmd/dep - - dep ensure +before_install: + - if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi + - if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi + - go mod download script: make generate; if [ -z "$FIX_TEST" ]; then make build; make; else make build_accept; make $FIX_TEST; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index dcc635229..e1d5c20d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -172,3 +172,5 @@ BUG FIXES ## 0.1.0 (February 21, 2016) * Initial release + +## LongBridge Feature \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d87ed08c1..56a325da7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,12 +2,14 @@ We welcome pull requests, bug fixes and issue reports. +Please direct questions about using the library to the [Mailing List](https://groups.google.com/forum/#!forum/long-bridge). + Before proposing a large change, please discuss your change by raising an issue. ## Submitting Changes * Push your changes to a topic branch in your fork of the repository -* Submit a pull request to the repository in the QuickFIXGo Organization +* Submit a pull request to the repository in the long-bridge Organization ## Notes diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 156d40f26..000000000 --- a/Gopkg.lock +++ /dev/null @@ -1,51 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - name = "github.com/davecgh/go-spew" - packages = ["spew"] - revision = "346938d642f2ec3594ed81d874461961cd0faa76" - version = "v1.1.0" - -[[projects]] - name = "github.com/mattn/go-sqlite3" - packages = ["."] - revision = "ca5e3819723d8eeaf170ad510e7da1d6d2e94a08" - version = "v1.2.0" - -[[projects]] - name = "github.com/pmezard/go-difflib" - packages = ["difflib"] - revision = "792786c7400a136282c1664665ae0a8db921c6c2" - version = "v1.0.0" - -[[projects]] - branch = "master" - name = "github.com/shopspring/decimal" - packages = ["."] - revision = "aed1bfe463fa3c9cc268d60dcc1491db613bff7e" - -[[projects]] - branch = "master" - name = "github.com/stretchr/objx" - packages = ["."] - revision = "1a9d0bb9f541897e62256577b352fdbc1fb4fd94" - -[[projects]] - name = "github.com/stretchr/testify" - packages = ["assert","mock","require","suite"] - revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" - version = "v1.1.4" - -[[projects]] - branch = "master" - name = "golang.org/x/net" - packages = ["context"] - revision = "a04bdaca5b32abe1c069418fb7088ae607de5bd0" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - inputs-digest = "6efc9f467166be5af0c9b9f4b98d7860ba12b50ce641a2fba765049bd1ea4f27" - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index 3385a5e25..000000000 --- a/Gopkg.toml +++ /dev/null @@ -1,34 +0,0 @@ - -# Gopkg.toml example -# -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" - - -[[constraint]] - name = "github.com/mattn/go-sqlite3" - version = "1.2.0" - -[[constraint]] - name = "github.com/shopspring/decimal" - branch = "master" - -[[constraint]] - name = "github.com/stretchr/testify" - version = "1.1.4" diff --git a/Makefile b/Makefile index d513ded8a..c883e5bd0 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ all: vet test -generate: +clean: + rm -rf gen + +generate: clean mkdir -p gen; cd gen; go run ../cmd/generate-fix/generate-fix.go ../spec/*.xml generate-dist: diff --git a/README.md b/README.md index f9a59056f..880eac602 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ QuickFIX/Go =========== -[![GoDoc](https://godoc.org/github.com/quickfixgo/quickfix?status.png)](https://godoc.org/github.com/quickfixgo/quickfix) [![Build Status](https://travis-ci.org/quickfixgo/quickfix.svg?branch=master)](https://travis-ci.org/quickfixgo/quickfix) [![Go Report Card](https://goreportcard.com/badge/github.com/quickfixgo/quickfix)](https://goreportcard.com/report/github.com/quickfixgo/quickfix) +[![GoDoc](https://godoc.org/github.com/quickfixgo/quickfix?status.png)](https://godoc.org/github.com/quickfixgo/quickfix) [![Build Status](https://travis-ci.org/long-bridge/quickfix.svg?branch=master)](https://travis-ci.org/long-bridge/quickfix) [![Go Report Card](https://goreportcard.com/badge/github.com/quickfixgo/quickfix)](https://goreportcard.com/report/github.com/quickfixgo/quickfix) -- Website: http://www.quickfixgo.org -- Mailing list: [Google Groups](https://groups.google.com/forum/#!forum/quickfixgo) +- Website: http://www.long-bridge.org +- Mailing list: [Google Groups](https://groups.google.com/forum/#!forum/long-bridge) Open Source [FIX Protocol](http://www.fixprotocol.org/) library implemented in Go Getting Started and Documentation --------------------------------- -* [User Manual](http://quickfixgo.org/docs) +* [User Manual](http://long-bridge.org/docs) * [API Documentation](https://godoc.org/github.com/quickfixgo/quickfix) ### Installation @@ -28,24 +28,24 @@ To update QuickFIX/Go to the latest version, use `go get -u github.com/quickfixg ### Example Apps -See [examples](https://github.com/quickfixgo/examples) for some simple examples of using QuickFIX/Go. +See [examples](https://github.com/long-bridge/examples) for some simple examples of using QuickFIX/Go. ### FIX Message Generation QuickFIX/Go includes separate packages for tags, fields, enums, messages, and message components generated from the FIX 4.0 - FIX5.0SP2 specs. See: -* [github.com/quickfixgo/tag](https://github.com/quickfixgo/tag) -* [github.com/quickfixgo/field](https://github.com/quickfixgo/field) -* [github.com/quickfixgo/enum](https://github.com/quickfixgo/enum) -* [github.com/quickfixgo/fix40](https://github.com/quickfixgo/fix40) -* [github.com/quickfixgo/fix41](https://github.com/quickfixgo/fix41) -* [github.com/quickfixgo/fix42](https://github.com/quickfixgo/fix42) -* [github.com/quickfixgo/fix43](https://github.com/quickfixgo/fix43) -* [github.com/quickfixgo/fix44](https://github.com/quickfixgo/fix44) -* [github.com/quickfixgo/fix50](https://github.com/quickfixgo/fix50) -* [github.com/quickfixgo/fix50sp1](https://github.com/quickfixgo/fix50sp1) -* [github.com/quickfixgo/fix50sp2](https://github.com/quickfixgo/fix50sp2) -* [github.com/quickfixgo/fixt11](https://github.com/quickfixgo/fixt11) +* [github.com/long-bridge/tag](https://github.com/long-bridge/tag) +* [github.com/long-bridge/field](https://github.com/long-bridge/field) +* [github.com/long-bridge/enum](https://github.com/long-bridge/enum) +* [github.com/long-bridge/fix40](https://github.com/long-bridge/fix40) +* [github.com/long-bridge/fix41](https://github.com/long-bridge/fix41) +* [github.com/long-bridge/fix42](https://github.com/long-bridge/fix42) +* [github.com/long-bridge/fix43](https://github.com/long-bridge/fix43) +* [github.com/long-bridge/fix44](https://github.com/long-bridge/fix44) +* [github.com/long-bridge/fix50](https://github.com/long-bridge/fix50) +* [github.com/long-bridge/fix50sp1](https://github.com/long-bridge/fix50sp1) +* [github.com/long-bridge/fix50sp2](https://github.com/long-bridge/fix50sp2) +* [github.com/long-bridge/fixt11](https://github.com/long-bridge/fixt11) For most FIX applications, these generated resources are sufficient. Custom FIX applications may generate source specific to the FIX spec of that application using the `generate-fix` tool included with QuickFIX/Go. @@ -54,24 +54,16 @@ Following installation, `generate-fix` is installed to `$GOPATH/bin/generate-fix Developing QuickFIX/Go ---------------------- -If you wish to work on QuickFIX/Go itself, you will first need [Go](http://www.golang.org) installed on your machine (version 1.6+ is *required*). +If you wish to work on QuickFIX/Go itself, you will first need [Go](http://www.golang.org) installed and configured on your machine (version 1.13+ is preferred, but the minimum required version is 1.6). -For local dev first make sure Go is properly installed, including setting up a [GOPATH](http://golang.org/doc/code.html#GOPATH). - -Next, using [Git](https://git-scm.com/), clone this repository into `$GOPATH/src/github.com/quickfixgo/quickfix`. +Next, using [Git](https://git-scm.com/), clone the repository via `git clone git@github.com:long-bridge/quickfix.git` ### Installing Dependencies -QuickFIX/Go uses [dep](https://github.com/golang/dep) to manage the vendored dependencies. Install dep with `go get`: - -```sh -$ go get -u github.com/golang/dep/cmd/dep -``` - -Run `dep ensure` to install the correct versioned dependencies into `vendor/`, which Go 1.6+ automatically recognizes and loads. +As of Go version 1.13, QuickFIX/Go uses [modules](https://github.com/golang/go/wiki/Modules) to manage dependencies. You may require `GO111MODULE=on`. To install dependencies, run ```sh -$ $GOPATH/bin/dep ensure +go mod download ``` **Note:** No vendored dependencies are included in the QuickFIX/Go source. @@ -88,7 +80,7 @@ If this exits with exit status 0, then everything is working! ### Generated Code -Generated code from the FIX40-FIX50SP2 specs are available as separate repos under the [QuickFIX/Go organization](https://github.com/quickfixgo). The source specifications for this generated code is located in `spec/`. Generated code can be identified by the `.generated.go` suffix. Any changes to generated code must be captured by changes to source in `cmd/generate-fix`. After making changes to the code generator source, run the following to re-generate the source +Generated code from the FIX40-FIX50SP2 specs are available as separate repos under the [QuickFIX/Go organization](https://github.com/long-bridge). The source specifications for this generated code is located in `spec/`. Generated code can be identified by the `.generated.go` suffix. Any changes to generated code must be captured by changes to source in `cmd/generate-fix`. After making changes to the code generator source, run the following to re-generate the source ```sh $ make generate-dist @@ -117,37 +109,22 @@ To run acceptance tests, If you are developing QuickFIX/Go, there are a few tasks you might need to perform related to dependencies. -#### Adding a dependency - -If you are adding a dependency, you will need to update the dep manifest in the same Pull Request as the code that depends on it. You should do this in a separate commit from your code, as this makes PR review easier and Git history simpler to read in the future. +#### Adding/updating a dependency -To add a dependency: - -1. Add the dependency using `dep`: -```bash -$ dep ensure -add github.com/foo/bar -``` -2. Review the changes in git and commit them. +If you are adding or updating a dependency, you will need to update the `go.mod` and `go.sum` in the same Pull Request as the code that depends on it. You should do this in a separate commit from your code, as this makes PR review easier and Git history simpler to read in the future. -#### Updating a dependency - -To update a dependency to the latest version allowed by constraints in `Gopkg.toml`: - -1. Run: -```bash -$ dep ensure -update github.com/foo/bar +1. Add or update the dependency like usual: +```sh +go get -u github.com/foo/bar ``` -2. Review the changes in git and commit them. - -To change the allowed version/branch/revision of a dependency: - -1. Manually edit `Gopkg.toml` -2. Run: -```bash -$ dep ensure +2. Update the module-related files: +```sh +go mod tidy ``` 3. Review the changes in git and commit them. +Note that to specify a specific revision, you can manually edit the `go.mod` file and run `go mod tidy` + Licensing --------- diff --git a/_sql/postgresql/messages_table.sql b/_sql/postgresql/messages_table.sql index 38507c091..e87209e85 100644 --- a/_sql/postgresql/messages_table.sql +++ b/_sql/postgresql/messages_table.sql @@ -9,6 +9,7 @@ CREATE TABLE messages ( session_qualifier VARCHAR(64) NOT NULL, msgseqnum INTEGER NOT NULL, message TEXT NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (beginstring, sendercompid, sendersubid, senderlocid, targetcompid, targetsubid, targetlocid, session_qualifier, msgseqnum) diff --git a/_test/echo_server.go b/_test/echo_server.go index ddd729789..48d684740 100644 --- a/_test/echo_server.go +++ b/_test/echo_server.go @@ -7,9 +7,10 @@ import ( "os" "os/signal" - "github.com/quickfixgo/quickfix" "github.com/quickfixgo/quickfix/gen/field" "github.com/quickfixgo/quickfix/gen/tag" + + "github.com/quickfixgo/quickfix" ) var router *quickfix.MessageRouter = quickfix.NewMessageRouter() @@ -32,6 +33,10 @@ func (e *EchoApplication) OnLogout(sessionID quickfix.SessionID) { func (e EchoApplication) ToAdmin(msgBuilder *quickfix.Message, sessionID quickfix.SessionID) { } +func (e EchoApplication) OnEvent(sessionID quickfix.SessionID, tp quickfix.EventType, ev interface{}) { + +} + func (e EchoApplication) ToApp(msgBuilder *quickfix.Message, sessionID quickfix.SessionID) (err error) { return } diff --git a/accepter_test.go b/accepter_test.go new file mode 100644 index 000000000..6924447fe --- /dev/null +++ b/accepter_test.go @@ -0,0 +1,56 @@ +package quickfix + +import ( + "net" + "testing" + + "github.com/armon/go-proxyproto" + "github.com/stretchr/testify/assert" +) + +func TestAcceptor_Start(t *testing.T) { + settingsWithTCPProxy := NewSettings() + settingsWithTCPProxy.GlobalSettings().Set("UseTCPProxy", "Y") + + settingsWithNoTCPProxy := NewSettings() + settingsWithNoTCPProxy.GlobalSettings().Set("UseTCPProxy", "N") + + genericSettings := NewSettings() + + const ( + GenericListener = iota + ProxyListener + ) + + acceptorStartTests := []struct { + name string + settings *Settings + listenerType int + }{ + {"with TCP proxy set", settingsWithTCPProxy, ProxyListener}, + {"with no TCP proxy set", settingsWithNoTCPProxy, GenericListener}, + {"no TCP proxy configuration set", genericSettings, GenericListener}, + } + + for _, tt := range acceptorStartTests { + t.Run(tt.name, func(t *testing.T) { + tt.settings.GlobalSettings().Set("SocketAcceptPort", "5001") + + acceptor := &Acceptor{settings: tt.settings} + if err := acceptor.Start(); err != nil { + assert.NotNil(t, err) + } + if tt.listenerType == ProxyListener { + _, ok := acceptor.listener.(*proxyproto.Listener) + assert.True(t, ok) + } + + if tt.listenerType == GenericListener { + _, ok := acceptor.listener.(*net.TCPListener) + assert.True(t, ok) + } + + acceptor.Stop() + }) + } +} \ No newline at end of file diff --git a/acceptor.go b/acceptor.go index c9138355f..ac0ab6e52 100644 --- a/acceptor.go +++ b/acceptor.go @@ -10,23 +10,37 @@ import ( "strconv" "sync" + "github.com/armon/go-proxyproto" "github.com/quickfixgo/quickfix/config" ) //Acceptor accepts connections from FIX clients and manages the associated sessions. type Acceptor struct { - app Application - settings *Settings - logFactory LogFactory - storeFactory MessageStoreFactory - globalLog Log - sessions map[SessionID]*session - sessionGroup sync.WaitGroup - listener net.Listener - listenerShutdown sync.WaitGroup + app Application + settings *Settings + logFactory LogFactory + storeFactory MessageStoreFactory + globalLog Log + sessions map[SessionID]*session + sessionGroup sync.WaitGroup + listener net.Listener + listenerShutdown sync.WaitGroup + dynamicSessions bool + dynamicQualifier bool + dynamicQualifierCount int + dynamicSessionChan chan *session + sessionAddr map[SessionID]net.Addr + connectionValidator ConnectionValidator sessionFactory } +// ConnectionValidator is an interface allowing to implement a custom authentication logic. +type ConnectionValidator interface { + // Validate the connection for validity. This can be a part of authentication process. + // For example, you may tie up a SenderCompID to an IP range, or to a specific TLS certificate as a part of mTLS. + Validate(netConn net.Conn, session SessionID) error +} + //Start accepting connections. func (a *Acceptor) Start() error { socketAcceptHost := "" @@ -47,11 +61,24 @@ func (a *Acceptor) Start() error { return err } + var useTCPProxy bool + if a.settings.GlobalSettings().HasSetting(config.UseTCPProxy) { + if useTCPProxy, err = a.settings.GlobalSettings().BoolSetting(config.UseTCPProxy); err != nil { + return err + } + } + address := net.JoinHostPort(socketAcceptHost, strconv.Itoa(socketAcceptPort)) if tlsConfig != nil { if a.listener, err = tls.Listen("tcp", address, tlsConfig); err != nil { return err } + } else if useTCPProxy { + listener, err := net.Listen("tcp", address) + if err != nil { + return err + } + a.listener = &proxyproto.Listener{Listener: listener} } else { if a.listener, err = net.Listen("tcp", address); err != nil { return err @@ -66,7 +93,14 @@ func (a *Acceptor) Start() error { a.sessionGroup.Done() }() } - + if a.dynamicSessions { + a.dynamicSessionChan = make(chan *session) + a.sessionGroup.Add(1) + go func() { + a.dynamicSessionsLoop() + a.sessionGroup.Done() + }() + } a.listenerShutdown.Add(1) go a.listenForConnections() return nil @@ -80,12 +114,21 @@ func (a *Acceptor) Stop() { a.listener.Close() a.listenerShutdown.Wait() + if a.dynamicSessions { + close(a.dynamicSessionChan) + } for _, session := range a.sessions { session.stop() } a.sessionGroup.Wait() } +//Get remote IP address for a given session. +func (a *Acceptor) RemoteAddr(sessionID SessionID) (net.Addr, bool) { + addr, ok := a.sessionAddr[sessionID] + return addr, ok +} + //NewAcceptor creates and initializes a new Acceptor. func NewAcceptor(app Application, storeFactory MessageStoreFactory, settings *Settings, logFactory LogFactory) (a *Acceptor, err error) { a = &Acceptor{ @@ -94,6 +137,18 @@ func NewAcceptor(app Application, storeFactory MessageStoreFactory, settings *Se settings: settings, logFactory: logFactory, sessions: make(map[SessionID]*session), + sessionAddr: make(map[SessionID]net.Addr), + } + if a.settings.GlobalSettings().HasSetting(config.DynamicSessions) { + if a.dynamicSessions, err = settings.globalSettings.BoolSetting(config.DynamicSessions); err != nil { + return + } + + if a.settings.GlobalSettings().HasSetting(config.DynamicQualifier) { + if a.dynamicQualifier, err = settings.globalSettings.BoolSetting(config.DynamicQualifier); err != nil { + return + } + } } if a.globalLog, err = logFactory.Create(); err != nil { @@ -220,12 +275,36 @@ func (a *Acceptor) handleConnection(netConn net.Conn) { SenderCompID: string(targetCompID), SenderSubID: string(targetSubID), SenderLocationID: string(targetLocationID), TargetCompID: string(senderCompID), TargetSubID: string(senderSubID), TargetLocationID: string(senderLocationID), } + + // We have a session ID and a network connection. This seems to be a good place for any custom authentication logic. + if a.connectionValidator != nil { + if err := a.connectionValidator.Validate(netConn, sessID); err != nil { + a.globalLog.OnEventf("Unable to validate a connection %v", err.Error()) + return + } + } + + if a.dynamicQualifier { + a.dynamicQualifierCount++ + sessID.Qualifier = strconv.Itoa(a.dynamicQualifierCount) + } session, ok := a.sessions[sessID] if !ok { - a.globalLog.OnEventf("Session %v not found for incoming message: %s", sessID, msgBytes) - return + if !a.dynamicSessions { + a.globalLog.OnEventf("Session %v not found for incoming message: %s", sessID, msgBytes) + return + } + dynamicSession, err := a.sessionFactory.createSession(sessID, a.storeFactory, a.settings.globalSettings.clone(), a.logFactory, a.app) + if err != nil { + a.globalLog.OnEventf("Dynamic session %v failed to create: %v", sessID, err) + return + } + a.dynamicSessionChan <- dynamicSession + session = dynamicSession + defer session.stop() } + a.sessionAddr[sessID] = netConn.RemoteAddr() msgIn := make(chan fixIn) msgOut := make(chan []byte) @@ -241,3 +320,62 @@ func (a *Acceptor) handleConnection(netConn net.Conn) { writeLoop(netConn, msgOut, a.globalLog) } + +func (a *Acceptor) dynamicSessionsLoop() { + var id int + var sessions = map[int]*session{} + var complete = make(chan int) + defer close(complete) +LOOP: + for { + select { + case session, ok := <-a.dynamicSessionChan: + if !ok { + for _, oldSession := range sessions { + oldSession.stop() + } + break LOOP + } + id++ + sessionID := id + sessions[sessionID] = session + go func() { + session.run() + err := UnregisterSession(session.sessionID) + if err != nil { + a.globalLog.OnEventf("Unregister dynamic session %v failed: %v", session.sessionID, err) + return + } + complete <- sessionID + }() + case id := <-complete: + session, ok := sessions[id] + if ok { + delete(a.sessionAddr, session.sessionID) + delete(sessions, id) + } else { + a.globalLog.OnEventf("Missing dynamic session %v!", id) + } + } + } + + if len(sessions) == 0 { + return + } + + for id := range complete { + delete(sessions, id) + if len(sessions) == 0 { + return + } + } +} + +// SetConnectionValidator sets an optional connection validator. +// Use it when you need a custom authentication logic that includes lower level interactions, +// like mTLS auth or IP whitelistening. +// To remove a previously set validator call it with a nil value: +// a.SetConnectionValidator(nil) +func (a *Acceptor) SetConnectionValidator(validator ConnectionValidator) { + a.connectionValidator = validator +} diff --git a/application.go b/application.go index 09b3b49a3..cb39257cd 100644 --- a/application.go +++ b/application.go @@ -12,6 +12,8 @@ type Application interface { //Notification of a session logging off or disconnecting. OnLogout(sessionID SessionID) + OnEvent(sessionID SessionID, tp EventType, ev interface{}) + //Notification of admin message being sent to target. ToAdmin(message *Message, sessionID SessionID) diff --git a/backup_store.go b/backup_store.go new file mode 100644 index 000000000..f035d3544 --- /dev/null +++ b/backup_store.go @@ -0,0 +1,124 @@ +package quickfix + +import ( + "fmt" +) + +const ( + OperationSetNextSenderMsgSeqNum int = iota + 1 + OperationSetNextTargetMsgSeqNum + OperationSaveMessage + OperationReset +) + +type BackupMessage struct { + Operation int + SeqNum int + Msg []byte +} + +type backupStoreFactory struct { + messagesQueue chan *BackupMessage + backupFactory MessageStoreFactory +} + +type backupStore struct { + messagesQueue chan *BackupMessage + store MessageStore +} + +func NewBackupStoreFactory(messagesQueue chan *BackupMessage, backupFactory MessageStoreFactory) *backupStoreFactory { + return &backupStoreFactory{messagesQueue: messagesQueue, backupFactory: backupFactory} +} + +func (f backupStoreFactory) Create(sessionID SessionID) (msgStore *backupStore, err error) { + backupStore, err := f.backupFactory.Create(sessionID) + if err != nil { + return nil, err + } + + return newBackupStore(backupStore, f.messagesQueue), nil +} + +func newBackupStore(store MessageStore, messagesQueue chan *BackupMessage) *backupStore { + backup := &backupStore{messagesQueue: messagesQueue, store: store} + + backup.start() + + return backup +} + +func (s *backupStore) start() { + if s == nil { + return + } + + go func() { + for message := range s.messagesQueue { + switch message.Operation { + case OperationSetNextSenderMsgSeqNum: + if err := s.store.SetNextSenderMsgSeqNum(message.SeqNum); err != nil { + } + case OperationSetNextTargetMsgSeqNum: + if err := s.store.SetNextTargetMsgSeqNum(message.SeqNum); err != nil { + } + case OperationSaveMessage: + if err := s.store.SaveMessage(message.SeqNum, message.Msg); err != nil { + } + case OperationReset: + if err := s.store.Reset(); err != nil { + } + default: + fmt.Printf("backup store: unsupported operation(%v)\n", message.Operation) + } + } + }() +} + +func (s *backupStore) SetNextSenderMsgSeqNum(next int) { + if s == nil { + return + } + + select { + case s.messagesQueue <- &BackupMessage{Operation: OperationSetNextSenderMsgSeqNum, SeqNum: next}: + default: + fmt.Println("encountering a large amount of traffic, drop the SetNextSenderMsgSeqNum operation") + } +} + +func (s *backupStore) SetNextTargetMsgSeqNum(next int) { + if s == nil { + return + } + + select { + case s.messagesQueue <- &BackupMessage{Operation: OperationSetNextTargetMsgSeqNum, SeqNum: next}: + default: + fmt.Println("encountering a large amount of traffic, drop the SetNextTargetMsgSeqNum operation") + } +} + +func (s *backupStore) SaveMessage(seqNum int, msg []byte) { + if s == nil { + return + } + + select { + case s.messagesQueue <- &BackupMessage{Operation: OperationSaveMessage, SeqNum: seqNum, Msg: msg}: + default: + fmt.Println("encountering a large amount of traffic, drop the SaveMessage operation") + } +} + +func (s *backupStore) Reset() { + if s == nil { + return + } + + select { + case s.messagesQueue <- &BackupMessage{Operation: OperationReset}: + default: + fmt.Println("encountering a large amount of traffic, drop the Reset operation") + } +} diff --git a/cmd/generate-fix/internal/generate.go b/cmd/generate-fix/internal/generate.go index 9fbefea29..011eae371 100644 --- a/cmd/generate-fix/internal/generate.go +++ b/cmd/generate-fix/internal/generate.go @@ -13,6 +13,7 @@ import ( var ( useFloat = flag.Bool("use-float", false, "By default, FIX float fields are represented as arbitrary-precision fixed-point decimal numbers. Set to 'true' to instead generate FIX float fields as float64 values.") + pkgRoot = flag.String("pkg-root", "github.com/long-bridge", "Set a string here to provide a custom import path for generated packages.") tabWidth = 8 printerMode = printer.UseSpaces | printer.TabIndent ) diff --git a/cmd/generate-fix/internal/helpers.go b/cmd/generate-fix/internal/helpers.go index 13d94bf7f..8fe4cd4c6 100644 --- a/cmd/generate-fix/internal/helpers.go +++ b/cmd/generate-fix/internal/helpers.go @@ -1,21 +1,6 @@ package internal -import ( - "os" - "path" - "strings" -) - // getImportPathRoot returns the root path to use in import statements. -// The root path is determined by stripping "$GOPATH/src/" from the current -// working directory. For example, when generating code within the QuickFIX/Go -// source tree, the returned root path will be "github.com/quickfixgo/quickfix". func getImportPathRoot() string { - pwd, err := os.Getwd() - if err != nil { - panic(err) - } - goSrcPath := path.Join(os.Getenv("GOPATH"), "src") - importPathRoot := strings.Replace(pwd, goSrcPath, "", 1) - return strings.TrimLeft(importPathRoot, "/") + return *pkgRoot } diff --git a/cmd/generate-fix/internal/templates.go b/cmd/generate-fix/internal/templates.go index 95aa5276e..cae2d567d 100644 --- a/cmd/generate-fix/internal/templates.go +++ b/cmd/generate-fix/internal/templates.go @@ -57,7 +57,7 @@ Set{{ .Name }}(f {{ .Name }}RepeatingGroup){ {{ define "setters" }} {{ range .Fields }} -//Set{{ .Name }} sets {{ .Name }}, Tag {{ .Tag }} +// Set{{ .Name }} sets {{ .Name }}, Tag {{ .Tag }}. func ({{ template "receiver" }} {{ $.Name }}) {{ if .IsGroup }}{{ template "groupsetter" . }}{{ else }}{{ template "fieldsetter" . }}{{ end }} {{ end }}{{ end }} @@ -97,13 +97,13 @@ Get{{ .Name }}() ({{ .Name }}RepeatingGroup, quickfix.MessageRejectError) { {{ define "getters" }} {{ range .Fields }} -//Get{{ .Name }} gets {{ .Name }}, Tag {{ .Tag }} +// Get{{ .Name }} gets {{ .Name }}, Tag {{ .Tag }}. func ({{ template "receiver" }} {{ $.Name }}) {{if .IsGroup}}{{ template "groupgetter" . }}{{ else }}{{ template "fieldvaluegetter" .}}{{ end }} {{ end }}{{ end }} {{ define "hasers" }} {{range .Fields}} -//Has{{ .Name}} returns true if {{ .Name}} is present, Tag {{ .Tag}} +// Has{{ .Name}} returns true if {{ .Name}} is present, Tag {{ .Tag}}. func ({{ template "receiver" }} {{ $.Name }}) Has{{ .Name}}() bool { return {{ template "receiver" }}.Has(tag.{{ .Name}}) } @@ -121,7 +121,7 @@ quickfix.GroupTemplate{ {{ define "groups" }} {{ range .Fields }} {{ if .IsGroup }} -//{{ .Name }} is a repeating group element, Tag {{ .Tag }} +// {{ .Name }} is a repeating group element, Tag {{ .Tag }}. type {{ .Name }} struct { *quickfix.Group } @@ -131,24 +131,24 @@ type {{ .Name }} struct { {{ template "hasers" . }} {{ template "groups" . }} -//{{ .Name }}RepeatingGroup is a repeating group, Tag {{ .Tag }} +// {{ .Name }}RepeatingGroup is a repeating group, Tag {{ .Tag }}. type {{ .Name }}RepeatingGroup struct { *quickfix.RepeatingGroup } -//New{{ .Name }}RepeatingGroup returns an initialized, {{ .Name }}RepeatingGroup +// New{{ .Name }}RepeatingGroup returns an initialized, {{ .Name }}RepeatingGroup. func New{{ .Name }}RepeatingGroup() {{ .Name }}RepeatingGroup { return {{ .Name }}RepeatingGroup{ quickfix.NewRepeatingGroup(tag.{{ .Name }}, {{ template "group_template" .Fields }})} } -//Add create and append a new {{ .Name }} to this group +// Add create and append a new {{ .Name }} to this group. func ({{ template "receiver" }} {{ .Name }}RepeatingGroup) Add() {{ .Name }} { g := {{ template "receiver" }}.RepeatingGroup.Add() return {{ .Name }}{g} } -//Get returns the ith {{ .Name }} in the {{ .Name }}RepeatinGroup +// Get returns the ith {{ .Name }} in the {{ .Name }}RepeatinGroup. func ({{ template "receiver" }} {{ .Name}}RepeatingGroup) Get(i int) {{ .Name }} { return {{ .Name }}{ {{ template "receiver" }}.RepeatingGroup.Get(i) } } @@ -174,12 +174,12 @@ import( "{{ importRootPath }}/tag" ) -//Header is the {{ .Package }} Header type +// Header is the {{ .Package }} Header type. type Header struct { *quickfix.Header } -//NewHeader returns a new, initialized Header instance +// NewHeader returns a new, initialized Header instance. func NewHeader(header *quickfix.Header) (h Header) { h.Header = header h.SetBeginString("{{ beginString .FIXSpec }}") @@ -209,7 +209,7 @@ import( "{{ importRootPath }}/tag" ) -//Trailer is the {{ .Package }} Trailer type +// Trailer is the {{ .Package }} Trailer type. type Trailer struct { *quickfix.Trailer } @@ -238,7 +238,7 @@ import( "{{ importRootPath }}/tag" ) -//{{ .Name }} is the {{ .FIXPackage }} {{ .Name }} type, MsgType = {{ .MsgType }} +// {{ .Name }} is the {{ .FIXPackage }} {{ .Name }} type, MsgType = {{ .MsgType }}. type {{ .Name }} struct { {{ .TransportPackage }}.Header *quickfix.Body @@ -246,7 +246,7 @@ type {{ .Name }} struct { Message *quickfix.Message } -//FromMessage creates a {{ .Name }} from a quickfix.Message instance +// FromMessage creates a {{ .Name }} from a quickfix.Message instance. func FromMessage(m *quickfix.Message) {{ .Name }} { return {{ .Name }}{ Header: {{ .TransportPackage}}.Header{&m.Header}, @@ -256,13 +256,13 @@ func FromMessage(m *quickfix.Message) {{ .Name }} { } } -//ToMessage returns a quickfix.Message instance +// ToMessage returns a quickfix.Message instance. func (m {{ .Name }}) ToMessage() *quickfix.Message { return m.Message } {{ $required_fields := requiredFields .MessageDef -}} -//New returns a {{ .Name }} initialized with the required fields for {{ .Name }} +// New returns a {{ .Name }} initialized with the required fields for {{ .Name }}. func New({{template "field_args" $required_fields }}) (m {{ .Name }}) { m.Message = quickfix.NewMessage() m.Header = {{ .TransportPackage }}.NewHeader(&m.Message.Header) @@ -277,10 +277,10 @@ func New({{template "field_args" $required_fields }}) (m {{ .Name }}) { return } -//A RouteOut is the callback type that should be implemented for routing Message +// A RouteOut is the callback type that should be implemented for routing Message. type RouteOut func(msg {{ .Name }}, sessionID quickfix.SessionID) quickfix.MessageRejectError -//Route returns the beginstring, message type, and MessageRoute for this Message type +// Route returns the beginstring, message type, and MessageRoute for this Message type. func Route(router RouteOut) (string, string, quickfix.MessageRoute) { r:=func(msg *quickfix.Message, sessionID quickfix.SessionID) quickfix.MessageRejectError { return router(FromMessage(msg), sessionID) @@ -319,28 +319,28 @@ import( {{- $base_type := quickfixType . -}} {{ if and .Enums (ne $base_type "FIXBoolean") }} -//{{ .Name }}Field is a enum.{{ .Name }} field +// {{ .Name }}Field is a enum.{{ .Name }} field. type {{ .Name }}Field struct { quickfix.FIXString } {{ else }} -//{{ .Name }}Field is a {{ .Type }} field +// {{ .Name }}Field is a {{ .Type }} field. type {{ .Name }}Field struct { quickfix.{{ $base_type }} } {{ end }} -//Tag returns tag.{{ .Name }} ({{ .Tag }}) +// Tag returns tag.{{ .Name }} ({{ .Tag }}). func (f {{ .Name }}Field) Tag() quickfix.Tag { return tag.{{ .Name }} } {{ if eq $base_type "FIXUTCTimestamp" }} -//New{{ .Name }} returns a new {{ .Name }}Field initialized with val +// New{{ .Name }} returns a new {{ .Name }}Field initialized with val. func New{{ .Name }}(val time.Time) {{ .Name }}Field { return New{{ .Name }}WithPrecision(val, quickfix.Millis) } -//New{{ .Name }}NoMillis returns a new {{ .Name }}Field initialized with val without millisecs +// New{{ .Name }}NoMillis returns a new {{ .Name }}Field initialized with val without millisecs. func New{{ .Name }}NoMillis(val time.Time) {{ .Name }}Field { return New{{ .Name }}WithPrecision(val, quickfix.Seconds) } -//New{{ .Name }}WithPrecision returns a new {{ .Name }}Field initialized with val of specified precision +// New{{ .Name }}WithPrecision returns a new {{ .Name }}Field initialized with val of specified precision. func New{{ .Name }}WithPrecision(val time.Time, precision quickfix.TimestampPrecision) {{ .Name }}Field { return {{ .Name }}Field{ quickfix.FIXUTCTimestamp{ Time: val, Precision: precision } } } @@ -350,12 +350,12 @@ func New{{ .Name }}(val enum.{{ .Name }}) {{ .Name }}Field { return {{ .Name }}Field{ quickfix.FIXString(val) } } {{ else if eq $base_type "FIXDecimal" }} -//New{{ .Name }} returns a new {{ .Name }}Field initialized with val and scale +// New{{ .Name }} returns a new {{ .Name }}Field initialized with val and scale. func New{{ .Name }}(val decimal.Decimal, scale int32) {{ .Name }}Field { return {{ .Name }}Field{ quickfix.FIXDecimal{ Decimal: val, Scale: scale} } } {{ else }} -//New{{ .Name }} returns a new {{ .Name }}Field initialized with val +// New{{ .Name }} returns a new {{ .Name }}Field initialized with val. func New{{ .Name }}(val {{ quickfixValueType $base_type }}) {{ .Name }}Field { return {{ .Name }}Field{ quickfix.{{ $base_type }}(val) } } @@ -386,7 +386,7 @@ func (f {{ .Name }}Field) Value() ({{ quickfixValueType $base_type }}) { package enum {{ range $ft := . }} {{ if $ft.Enums }} -//Enum values for {{ $ft.Name }} +// {{ $ft.Name }} field enumeration values. type {{ $ft.Name }} string const( {{ range $ft.Enums }} diff --git a/config/configuration.go b/config/configuration.go index a8b5ae2cf..edc55ed67 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -1,6 +1,6 @@ package config -//NOTE: Additions to this file should be made to both config/doc.go and http://www.quickfixgo.org/docs/ +//NOTE: Additions to this file should be made to both config/doc.go and http://www.long-bridge.org/docs/ //Const configuration settings const ( @@ -20,7 +20,16 @@ const ( SocketCertificateFile string = "SocketCertificateFile" SocketCAFile string = "SocketCAFile" SocketInsecureSkipVerify string = "SocketInsecureSkipVerify" + SocketServerName string = "SocketServerName" SocketMinimumTLSVersion string = "SocketMinimumTLSVersion" + SocketTimeout string = "SocketTimeout" + SocketUseSSL string = "SocketUseSSL" + ProxyType string = "ProxyType" + ProxyHost string = "ProxyHost" + ProxyPort string = "ProxyPort" + ProxyUser string = "ProxyUser" + ProxyPassword string = "ProxyPassword" + UseTCPProxy string = "UseTCPProxy" DefaultApplVerID string = "DefaultApplVerID" StartTime string = "StartTime" EndTime string = "EndTime" @@ -35,12 +44,17 @@ const ( ResetOnLogout string = "ResetOnLogout" ResetOnDisconnect string = "ResetOnDisconnect" ReconnectInterval string = "ReconnectInterval" + LogoutTimeout string = "LogoutTimeout" + LogonTimeout string = "LogonTimeout" HeartBtInt string = "HeartBtInt" FileLogPath string = "FileLogPath" FileStorePath string = "FileStorePath" + FileStoreSync string = "FileStoreSync" SQLStoreDriver string = "SQLStoreDriver" SQLStoreDataSourceName string = "SQLStoreDataSourceName" SQLStoreConnMaxLifetime string = "SQLStoreConnMaxLifetime" + MongoStoreConnection string = "MongoStoreConnection" + MongoStoreDatabase string = "MongoStoreDatabase" ValidateFieldsOutOfOrder string = "ValidateFieldsOutOfOrder" ResendRequestChunkSize string = "ResendRequestChunkSize" EnableLastMsgSeqNumProcessed string = "EnableLastMsgSeqNumProcessed" @@ -48,4 +62,8 @@ const ( TimeStampPrecision string = "TimeStampPrecision" MaxLatency string = "MaxLatency" PersistMessages string = "PersistMessages" + RejectInvalidMessage string = "RejectInvalidMessage" + DynamicSessions string = "DynamicSessions" + DynamicQualifier string = "DynamicQualifier" + SendRatePerSecond string = "SendRatePerSecond" ) diff --git a/config/doc.go b/config/doc.go index 48b4113b7..eff831de9 100644 --- a/config/doc.go +++ b/config/doc.go @@ -202,6 +202,14 @@ If set to N, fields that are out of order (i.e. body fields in the header, or he Defaults to Y. +RejectInvalidMessage + +If RejectInvalidMessage is set to N, zero errors will be thrown on reception of message that fails data dictionary validation. Valid Values: + Y + N + +Defaults to Y. + CheckLatency If set to Y, messages must be received from the counterparty within a defined number of seconds. It is useful to turn this off if a system uses localtime for it's timestamps instead of GMT. Valid Values: @@ -222,6 +230,18 @@ Time between reconnection attempts in seconds. Only used for initiators. Valu Defaults to 30 +LogoutTimeout + +Session setting for logout timeout in seconds. Only used for initiators. Value must be positive integer. + +Defaults to 2 + +LogonTimeout + +Session setting for logon timeout in seconds. Only used for initiators. Value must be positive integer. + +Defaults to 10 + HeartBtInt Heartbeat interval in seconds. Only used for initiators. Value must be positive integer. @@ -242,6 +262,16 @@ SocketConnectHost Alternate socket hosts for connecting to a session for failover, where n is a positive integer. (i.e.) SocketConnectHost1, SocketConnectHost2... must be consecutive and have a matching SocketConnectPort[n]. Value must be a valid IPv4 or IPv6 address or a domain name +SocketTimeout + +Duration of timeout for TLS handshake. Only used for initiators. + +Example Values: + SocketTimeout=30s # 30 seconds + SocketTimeout=60m # 60 minutes + +Defaults to 0(means nothing timeout). + SocketAcceptHost Socket host address for listening on incoming connections, only used for acceptors. By default acceptors listen on all available interfaces. @@ -262,10 +292,45 @@ SocketCAFile Optional root CA to use for secure TLS connections. For acceptors, client certificates will be verified against this CA. For initiators, clients will use the CA to verify the server certificate. If not configurated, initiators will verify the server certificate using the host's root CA set. +SocketServerName + +The expected server name on a returned certificate, unless SocketInsecureSkipVerify is true. This is for the TLS Server Name Indication extension. Initiator only. + SocketMinimumTLSVersion Specify the Minimum TLS version to use when creating a secure connection. The valid choices are SSL30, TLS10, TLS11, TLS12. Defaults to TLS12. +SocketUseSSL + +Use SSL for initiators even if client certificates are not present. If set to N or omitted, TLS will not be used if SocketPrivateKeyFile or SocketCertificateFile are not supplied. + +ProxyType + +Proxy type. Valid Values: + socks + +ProxyHost + +Proxy server IP address in the format of x.x.x.x or a domain name + +ProxyPort + +Proxy server port + +ProxyUser + +Proxy user + +ProxyPassword + +Proxy password + +UseTCPProxy + +Use TCP proxy for servers listening behind HAProxy of Amazon ELB load balancers. The server can then receive the address of the client instead of the load balancer's. Valid Values: + Y + N + PersistMessages If set to N, no messages will be persisted. This will force QuickFIX/Go to always send GapFills instead of resending messages. Use this if you know you never want to resend a message. Useful for market data streams. Valid Values: @@ -282,6 +347,14 @@ FileStorePath Directory to store sequence number and message files. Only used with FileStoreFactory. +MongoStoreConnection + +The MongoDB connection URL to use (see https://godoc.org/github.com/globalsign/mgo#Dial for the URL Format). Only used with MongoStoreFactory. + +MongoStoreDatabase + +The MongoDB-specific name of the database to use. Only used with MongoStoreFactory. + SQLStoreDriver The name of the database driver to use (see https://github.com/golang/go/wiki/SQLDrivers for the list of available drivers). Only used with SqlStoreFactory. diff --git a/connection.go b/connection.go index 4e5768a36..c52b55538 100644 --- a/connection.go +++ b/connection.go @@ -1,6 +1,9 @@ package quickfix -import "io" +import ( + "io" + "log" +) func writeLoop(connection io.Writer, messageOut chan []byte, log Log) { for { @@ -21,6 +24,7 @@ func readLoop(parser *parser, msgIn chan fixIn) { for { msg, err := parser.ReadMessage() if err != nil { + log.Println(`parser read message failed,conection readLoop just quit, error_info->`, err.Error()) return } msgIn <- fixIn{msg, parser.lastRead} diff --git a/datadictionary/datadictionary.go b/datadictionary/datadictionary.go index 410ca8ee1..98963e1b5 100644 --- a/datadictionary/datadictionary.go +++ b/datadictionary/datadictionary.go @@ -197,26 +197,7 @@ func (f FieldDef) childTags() []int { for _, f := range f.Fields { tags = append(tags, f.Tag()) - for _, t := range f.childTags() { - tags = append(tags, t) - } - } - - return tags -} - -func (f FieldDef) requiredChildTags() []int { - var tags []int - - for _, f := range f.Fields { - if !f.Required() { - continue - } - - tags = append(tags, f.Tag()) - for _, t := range f.requiredChildTags() { - tags = append(tags, t) - } + tags = append(tags, f.childTags()...) } return tags diff --git a/datadictionary/datadictionary_test.go b/datadictionary/datadictionary_test.go index 4179716c5..11d98bfca 100644 --- a/datadictionary/datadictionary_test.go +++ b/datadictionary/datadictionary_test.go @@ -82,7 +82,7 @@ func TestFieldsByTag(t *testing.T) { func TestEnumFieldsByTag(t *testing.T) { d, _ := dict() - f, _ := d.FieldTypeByTag[658] + f := d.FieldTypeByTag[658] var tests = []struct { Value string @@ -141,7 +141,7 @@ func TestDataDictionaryTrailer(t *testing.T) { func TestMessageRequiredTags(t *testing.T) { d, _ := dict() - nos, _ := d.Messages["D"] + nos := d.Messages["D"] var tests = []struct { *MessageDef @@ -169,7 +169,7 @@ func TestMessageRequiredTags(t *testing.T) { func TestMessageTags(t *testing.T) { d, _ := dict() - nos, _ := d.Messages["D"] + nos := d.Messages["D"] var tests = []struct { *MessageDef diff --git a/dialer.go b/dialer.go new file mode 100644 index 000000000..1645076bf --- /dev/null +++ b/dialer.go @@ -0,0 +1,56 @@ +package quickfix + +import ( + "fmt" + "net" + + "github.com/quickfixgo/quickfix/config" + "golang.org/x/net/proxy" +) + +func loadDialerConfig(settings *SessionSettings) (dialer proxy.Dialer, err error) { + stdDialer := &net.Dialer{} + if settings.HasSetting(config.SocketTimeout) { + if stdDialer.Timeout, err = settings.DurationSetting(config.SocketTimeout); err != nil { + return + } + } + dialer = stdDialer + + if !settings.HasSetting(config.ProxyType) { + return + } + + var proxyType string + if proxyType, err = settings.Setting(config.ProxyType); err != nil { + return + } + + switch proxyType { + case "socks": + var proxyHost string + var proxyPort int + if proxyHost, err = settings.Setting(config.ProxyHost); err != nil { + return + } else if proxyPort, err = settings.IntSetting(config.ProxyPort); err != nil { + return + } + + proxyAuth := new(proxy.Auth) + if settings.HasSetting(config.ProxyUser) { + if proxyAuth.User, err = settings.Setting(config.ProxyUser); err != nil { + return + } + } + if settings.HasSetting(config.ProxyPassword) { + if proxyAuth.Password, err = settings.Setting(config.ProxyPassword); err != nil { + return + } + } + + dialer, err = proxy.SOCKS5("tcp", fmt.Sprintf("%s:%d", proxyHost, proxyPort), proxyAuth, dialer) + default: + err = fmt.Errorf("unsupported proxy type %s", proxyType) + } + return +} diff --git a/dialer_test.go b/dialer_test.go new file mode 100644 index 000000000..510c18f04 --- /dev/null +++ b/dialer_test.go @@ -0,0 +1,76 @@ +package quickfix + +import ( + "net" + "testing" + "time" + + "github.com/quickfixgo/quickfix/config" + "github.com/stretchr/testify/suite" +) + +type DialerTestSuite struct { + suite.Suite + settings *Settings +} + +func TestDialerTestSuite(t *testing.T) { + suite.Run(t, new(DialerTestSuite)) +} + +func (s *DialerTestSuite) SetupTest() { + s.settings = NewSettings() +} + +func (s *DialerTestSuite) TestLoadDialerNoSettings() { + dialer, err := loadDialerConfig(s.settings.GlobalSettings()) + s.Require().Nil(err) + + stdDialer, ok := dialer.(*net.Dialer) + s.Require().True(ok) + s.Require().NotNil(stdDialer) + s.Zero(stdDialer.Timeout) +} + +func (s *DialerTestSuite) TestLoadDialerWithTimeout() { + s.settings.GlobalSettings().Set(config.SocketTimeout, "10s") + dialer, err := loadDialerConfig(s.settings.GlobalSettings()) + s.Require().Nil(err) + + stdDialer, ok := dialer.(*net.Dialer) + s.Require().True(ok) + s.Require().NotNil(stdDialer) + s.EqualValues(10*time.Second, stdDialer.Timeout) +} + +func (s *DialerTestSuite) TestLoadDialerInvalidProxy() { + s.settings.GlobalSettings().Set(config.ProxyType, "totallyinvalidproxytype") + _, err := loadDialerConfig(s.settings.GlobalSettings()) + s.Require().NotNil(err) +} + +func (s *DialerTestSuite) TestLoadDialerSocksProxy() { + s.settings.GlobalSettings().Set(config.ProxyType, "socks") + s.settings.GlobalSettings().Set(config.ProxyHost, "localhost") + s.settings.GlobalSettings().Set(config.ProxyPort, "31337") + dialer, err := loadDialerConfig(s.settings.GlobalSettings()) + s.Require().Nil(err) + s.Require().NotNil(dialer) + + _, ok := dialer.(*net.Dialer) + s.Require().False(ok) +} + +func (s *DialerTestSuite) TestLoadDialerSocksProxyInvalidHost() { + s.settings.GlobalSettings().Set(config.ProxyType, "socks") + s.settings.GlobalSettings().Set(config.ProxyPort, "31337") + _, err := loadDialerConfig(s.settings.GlobalSettings()) + s.Require().NotNil(err) +} + +func (s *DialerTestSuite) TestLoadDialerSocksProxyInvalidPort() { + s.settings.GlobalSettings().Set(config.ProxyType, "socks") + s.settings.GlobalSettings().Set(config.ProxyHost, "localhost") + _, err := loadDialerConfig(s.settings.GlobalSettings()) + s.Require().NotNil(err) +} diff --git a/doc.go b/doc.go index 76f7b7b71..e5451bb14 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,6 @@ /* Package quickfix is a full featured messaging engine for the FIX protocol. It is a 100% Go open source implementation of the popular C++ QuickFIX engine (http://quickfixengine.org). -User manual and additional information available at http://quickfixgo.org +User manual and additional information available at http://long-bridge.org */ package quickfix diff --git a/errors.go b/errors.go index af949ebd4..19ca16e50 100644 --- a/errors.go +++ b/errors.go @@ -33,6 +33,7 @@ type MessageRejectError interface { //RejectReason, tag 373 for session rejects, tag 380 for business rejects. RejectReason() int + BusinessRejectRefID() string RefTagID() *Tag IsBusinessReject() bool } @@ -50,20 +51,25 @@ func (RejectLogon) RefTagID() *Tag { return nil } //RejectReason implements MessageRejectError func (RejectLogon) RejectReason() int { return 0 } +//BusinessRejectRefID implements MessageRejectError +func (RejectLogon) BusinessRejectRefID() string { return "" } + //IsBusinessReject implements MessageRejectError func (RejectLogon) IsBusinessReject() bool { return false } type messageRejectError struct { - rejectReason int - text string - refTagID *Tag - isBusinessReject bool + rejectReason int + text string + businessRejectRefID string + refTagID *Tag + isBusinessReject bool } -func (e messageRejectError) Error() string { return e.text } -func (e messageRejectError) RefTagID() *Tag { return e.refTagID } -func (e messageRejectError) RejectReason() int { return e.rejectReason } -func (e messageRejectError) IsBusinessReject() bool { return e.isBusinessReject } +func (e messageRejectError) Error() string { return e.text } +func (e messageRejectError) RefTagID() *Tag { return e.refTagID } +func (e messageRejectError) RejectReason() int { return e.rejectReason } +func (e messageRejectError) BusinessRejectRefID() string { return e.businessRejectRefID } +func (e messageRejectError) IsBusinessReject() bool { return e.isBusinessReject } //NewMessageRejectError returns a MessageRejectError with the given error message, reject reason, and optional reftagid func NewMessageRejectError(err string, rejectReason int, refTagID *Tag) MessageRejectError { @@ -76,6 +82,12 @@ func NewBusinessMessageRejectError(err string, rejectReason int, refTagID *Tag) return messageRejectError{text: err, rejectReason: rejectReason, refTagID: refTagID, isBusinessReject: true} } +//NewBusinessMessageRejectErrorWithRefID returns a MessageRejectError with the given error mesage, reject reason, refID, and optional reftagid. +//Reject is treated as a business level reject +func NewBusinessMessageRejectErrorWithRefID(err string, rejectReason int, businessRejectRefID string, refTagID *Tag) MessageRejectError { + return messageRejectError{text: err, rejectReason: rejectReason, refTagID: refTagID, businessRejectRefID: businessRejectRefID, isBusinessReject: true} +} + //IncorrectDataFormatForValue returns an error indicating a field that cannot be parsed as the type required. func IncorrectDataFormatForValue(tag Tag) MessageRejectError { return NewMessageRejectError("Incorrect data format for value", rejectReasonIncorrectDataFormatForValue, &tag) diff --git a/errors_test.go b/errors_test.go index 56554af51..e612d3242 100644 --- a/errors_test.go +++ b/errors_test.go @@ -52,6 +52,33 @@ func TestNewBusinessMessageRejectError(t *testing.T) { } } +func TestNewBusinessMessageRejectErrorWithRefID(t *testing.T) { + var ( + expectedErrorString = "Custom error" + expectedRejectReason = 5 + expectedbusinessRejectRefID = "1" + expectedRefTagID Tag = 44 + expectedIsBusinessReject = true + ) + msgRej := NewBusinessMessageRejectErrorWithRefID(expectedErrorString, expectedRejectReason, expectedbusinessRejectRefID, &expectedRefTagID) + + if strings.Compare(msgRej.Error(), expectedErrorString) != 0 { + t.Errorf("expected: %s, got: %s\n", expectedErrorString, msgRej.Error()) + } + if msgRej.RejectReason() != expectedRejectReason { + t.Errorf("expected: %d, got: %d\n", expectedRejectReason, msgRej.RejectReason()) + } + if strings.Compare(msgRej.BusinessRejectRefID(), expectedbusinessRejectRefID) != 0 { + t.Errorf("expected: %s, got: %s\n", expectedbusinessRejectRefID, msgRej.BusinessRejectRefID()) + } + if *msgRej.RefTagID() != expectedRefTagID { + t.Errorf("expected: %d, got: %d\n", expectedRefTagID, msgRej.RefTagID()) + } + if msgRej.IsBusinessReject() != expectedIsBusinessReject { + t.Error("Expected IsBusinessReject to be true\n") + } +} + func TestIncorrectDataFormatForValue(t *testing.T) { var ( expectedErrorString = "Incorrect data format for value" diff --git a/event.go b/event.go new file mode 100644 index 000000000..8efb09f83 --- /dev/null +++ b/event.go @@ -0,0 +1,12 @@ +package quickfix + +type EventType int + +const ( + EventTypeLogon EventType = iota +) + +type EventLogon struct { + Addr string + TS int64 +} diff --git a/field_map.go b/field_map.go index a5e08d4e6..4284fc280 100644 --- a/field_map.go +++ b/field_map.go @@ -199,6 +199,19 @@ func (m *FieldMap) Clear() { } } +//CopyInto overwrites the given FieldMap with this one +func (m *FieldMap) CopyInto(to *FieldMap) { + to.tagLookup = make(map[Tag]field) + for tag, f := range m.tagLookup { + clone := make(field, 1) + clone[0] = f[0] + to.tagLookup[tag] = clone + } + to.tags = make([]Tag, len(m.tags)) + copy(to.tags, m.tags) + to.compare = m.compare +} + func (m *FieldMap) add(f field) { t := fieldTag(f) if _, ok := m.tagLookup[t]; !ok { diff --git a/field_map_test.go b/field_map_test.go index ce6a79119..f07f6f396 100644 --- a/field_map_test.go +++ b/field_map_test.go @@ -125,3 +125,53 @@ func TestFieldMap_BoolTypedSetAndGet(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "N", s) } + +func TestFieldMap_CopyInto(t *testing.T) { + var fMapA FieldMap + fMapA.initWithOrdering(headerFieldOrdering) + fMapA.SetString(9, "length") + fMapA.SetString(8, "begin") + fMapA.SetString(35, "msgtype") + fMapA.SetString(1, "a") + assert.Equal(t, []Tag{8, 9, 35, 1}, fMapA.sortedTags()) + + var fMapB FieldMap + fMapB.init() + fMapB.SetString(1, "A") + fMapB.SetString(3, "C") + fMapB.SetString(4, "D") + assert.Equal(t, fMapB.sortedTags(), []Tag{1, 3, 4}) + + fMapA.CopyInto(&fMapB) + + assert.Equal(t, []Tag{8, 9, 35, 1}, fMapB.sortedTags()) + + // new fields + s, err := fMapB.GetString(35) + assert.Nil(t, err) + assert.Equal(t, "msgtype", s) + + // existing fields overwritten + s, err = fMapB.GetString(1) + assert.Nil(t, err) + assert.Equal(t, "a", s) + + // old fields cleared + _, err = fMapB.GetString(3) + assert.NotNil(t, err) + + // check that ordering is overwritten + fMapB.SetString(2, "B") + assert.Equal(t, []Tag{8, 9, 35, 1, 2}, fMapB.sortedTags()) + + // updating the existing map doesn't affect the new + fMapA.init() + fMapA.SetString(1, "AA") + s, err = fMapB.GetString(1) + assert.Nil(t, err) + assert.Equal(t, "a", s) + fMapA.Clear() + s, err = fMapB.GetString(1) + assert.Nil(t, err) + assert.Equal(t, "a", s) +} diff --git a/file_log.go b/file_log.go index 02f27b001..202bf4012 100644 --- a/file_log.go +++ b/file_log.go @@ -35,8 +35,8 @@ type fileLogFactory struct { sessionLogPaths map[SessionID]string } -//NewFileLogFactory creates an instance of LogFactory that writes messages and events to file. -//The location of global and session log files is configured via FileLogPath. +// NewFileLogFactory creates an instance of LogFactory that writes messages and events to file. +// The location of global and session log files is configured via FileLogPath. func NewFileLogFactory(settings *Settings) (LogFactory, error) { logFactory := fileLogFactory{} @@ -97,6 +97,6 @@ func (f fileLogFactory) CreateSessionLog(sessionID SessionID) (Log, error) { return nil, fmt.Errorf("logger not defined for %v", sessionID) } - prefix := sessionIDFilenamePrefix(sessionID) + prefix := SessionIDFilenamePrefix(sessionID) return newFileLog(prefix, logPath) } diff --git a/file_log_test.go b/file_log_test.go index b807dca42..3358dc20a 100644 --- a/file_log_test.go +++ b/file_log_test.go @@ -11,7 +11,7 @@ import ( func TestFileLog_NewFileLogFactory(t *testing.T) { - factory, err := NewFileLogFactory(NewSettings()) + _, err := NewFileLogFactory(NewSettings()) if err == nil { t.Error("Should expect error when settings have no file log path") @@ -39,7 +39,7 @@ SessionQualifier=BS stringReader := strings.NewReader(cfg) settings, _ := ParseSettings(stringReader) - factory, err = NewFileLogFactory(settings) + factory, err := NewFileLogFactory(settings) if err != nil { t.Error("Did not expect error", err) diff --git a/filestore.go b/filestore.go index aa2f7cde3..479689bdc 100644 --- a/filestore.go +++ b/filestore.go @@ -2,12 +2,13 @@ package quickfix import ( "fmt" - "io/ioutil" + "io" "os" "path" "strconv" "time" + "github.com/pkg/errors" "github.com/quickfixgo/quickfix/config" ) @@ -17,7 +18,8 @@ type msgDef struct { } type fileStoreFactory struct { - settings *Settings + settings *Settings + backupFactory *backupStoreFactory } type fileStore struct { @@ -34,11 +36,18 @@ type fileStore struct { sessionFile *os.File senderSeqNumsFile *os.File targetSeqNumsFile *os.File + fileSync bool + backupStore *backupStore } // NewFileStoreFactory returns a file-based implementation of MessageStoreFactory -func NewFileStoreFactory(settings *Settings) MessageStoreFactory { - return fileStoreFactory{settings: settings} +func NewFileStoreFactory(settings *Settings, backupFactory *backupStoreFactory) MessageStoreFactory { + sfs := &fileStoreFactory{ + settings: settings, + backupFactory: backupFactory, + } + + return sfs } // Create creates a new FileStore implementation of the MessageStore interface @@ -51,15 +60,29 @@ func (f fileStoreFactory) Create(sessionID SessionID) (msgStore MessageStore, er if err != nil { return nil, err } - return newFileStore(sessionID, dirname) + var fsync bool + if sessionSettings.HasSetting(config.FileStoreSync) { + fsync, err = sessionSettings.BoolSetting(config.FileStoreSync) + if err != nil { + return nil, err + } + } else { + fsync = true //existing behavior is to fsync writes + } + backupStore, err := f.backupFactory.Create(sessionID) + if err != nil { + fmt.Printf("file store: failed to init backup store, err: %v\n", err) + } + + return newFileStore(sessionID, dirname, fsync, backupStore) } -func newFileStore(sessionID SessionID, dirname string) (*fileStore, error) { +func newFileStore(sessionID SessionID, dirname string, fileSync bool, backupStore *backupStore) (*fileStore, error) { if err := os.MkdirAll(dirname, os.ModePerm); err != nil { return nil, err } - sessionPrefix := sessionIDFilenamePrefix(sessionID) + sessionPrefix := SessionIDFilenamePrefix(sessionID) store := &fileStore{ sessionID: sessionID, @@ -70,6 +93,8 @@ func newFileStore(sessionID SessionID, dirname string) (*fileStore, error) { sessionFname: path.Join(dirname, fmt.Sprintf("%s.%s", sessionPrefix, "session")), senderSeqNumsFname: path.Join(dirname, fmt.Sprintf("%s.%s", sessionPrefix, "senderseqnums")), targetSeqNumsFname: path.Join(dirname, fmt.Sprintf("%s.%s", sessionPrefix, "targetseqnums")), + fileSync: fileSync, + backupStore: backupStore, } if err := store.Refresh(); err != nil { @@ -81,9 +106,12 @@ func newFileStore(sessionID SessionID, dirname string) (*fileStore, error) { // Reset deletes the store files and sets the seqnums back to 1 func (store *fileStore) Reset() error { - store.cache.Reset() + if err := store.cache.Reset(); err != nil { + return errors.Wrap(err, "cache reset") + } + if err := store.Close(); err != nil { - return err + return errors.Wrap(err, "close") } if err := removeFile(store.bodyFname); err != nil { return err @@ -100,12 +128,22 @@ func (store *fileStore) Reset() error { if err := removeFile(store.targetSeqNumsFname); err != nil { return err } - return store.Refresh() + + store.backupStore.Reset() + + if err := store.Refresh(); err != nil { + return err + } + + return nil } // Refresh closes the store files and then reloads from them func (store *fileStore) Refresh() (err error) { - store.cache.Reset() + if err = store.cache.Reset(); err != nil { + err = errors.Wrap(err, "cache reset") + return + } if err = store.Close(); err != nil { return err @@ -138,8 +176,14 @@ func (store *fileStore) Refresh() (err error) { } } - store.SetNextSenderMsgSeqNum(store.NextSenderMsgSeqNum()) - store.SetNextTargetMsgSeqNum(store.NextTargetMsgSeqNum()) + if err := store.SetNextSenderMsgSeqNum(store.NextSenderMsgSeqNum()); err != nil { + return errors.Wrap(err, "set next sender") + } + + if err := store.SetNextTargetMsgSeqNum(store.NextTargetMsgSeqNum()); err != nil { + return errors.Wrap(err, "set next target") + } + return nil } @@ -156,7 +200,7 @@ func (store *fileStore) populateCache() (creationTimePopulated bool, err error) } } - if timeBytes, err := ioutil.ReadFile(store.sessionFname); err == nil { + if timeBytes, err := os.ReadFile(store.sessionFname); err == nil { var ctime time.Time if err := ctime.UnmarshalText(timeBytes); err == nil { store.cache.creationTime = ctime @@ -164,15 +208,19 @@ func (store *fileStore) populateCache() (creationTimePopulated bool, err error) } } - if senderSeqNumBytes, err := ioutil.ReadFile(store.senderSeqNumsFname); err == nil { + if senderSeqNumBytes, err := os.ReadFile(store.senderSeqNumsFname); err == nil { if senderSeqNum, err := strconv.Atoi(string(senderSeqNumBytes)); err == nil { - store.cache.SetNextSenderMsgSeqNum(senderSeqNum) + if err = store.cache.SetNextSenderMsgSeqNum(senderSeqNum); err != nil { + return creationTimePopulated, errors.Wrap(err, "cache set next sender") + } } } - if targetSeqNumBytes, err := ioutil.ReadFile(store.targetSeqNumsFname); err == nil { + if targetSeqNumBytes, err := os.ReadFile(store.targetSeqNumsFname); err == nil { if targetSeqNum, err := strconv.Atoi(string(targetSeqNumBytes)); err == nil { - store.cache.SetNextTargetMsgSeqNum(targetSeqNum) + if err = store.cache.SetNextTargetMsgSeqNum(targetSeqNum); err != nil { + return creationTimePopulated, errors.Wrap(err, "cache set next target") + } } } @@ -180,7 +228,7 @@ func (store *fileStore) populateCache() (creationTimePopulated bool, err error) } func (store *fileStore) setSession() error { - if _, err := store.sessionFile.Seek(0, os.SEEK_SET); err != nil { + if _, err := store.sessionFile.Seek(0, io.SeekStart); err != nil { return fmt.Errorf("unable to rewind file: %s: %s", store.sessionFname, err.Error()) } @@ -191,21 +239,25 @@ func (store *fileStore) setSession() error { if _, err := store.sessionFile.Write(data); err != nil { return fmt.Errorf("unable to write to file: %s: %s", store.sessionFname, err.Error()) } - if err := store.sessionFile.Sync(); err != nil { - return fmt.Errorf("unable to flush file: %s: %s", store.sessionFname, err.Error()) + if store.fileSync { + if err := store.sessionFile.Sync(); err != nil { + return fmt.Errorf("unable to flush file: %s: %s", store.sessionFname, err.Error()) + } } return nil } func (store *fileStore) setSeqNum(f *os.File, seqNum int) error { - if _, err := f.Seek(0, os.SEEK_SET); err != nil { + if _, err := f.Seek(0, io.SeekStart); err != nil { return fmt.Errorf("unable to rewind file: %s: %s", f.Name(), err.Error()) } if _, err := fmt.Fprintf(f, "%019d", seqNum); err != nil { return fmt.Errorf("unable to write to file: %s: %s", f.Name(), err.Error()) } - if err := f.Sync(); err != nil { - return fmt.Errorf("unable to flush file: %s: %s", f.Name(), err.Error()) + if store.fileSync { + if err := f.Sync(); err != nil { + return fmt.Errorf("unable to flush file: %s: %s", f.Name(), err.Error()) + } } return nil } @@ -222,26 +274,62 @@ func (store *fileStore) NextTargetMsgSeqNum() int { // SetNextSenderMsgSeqNum sets the next MsgSeqNum that will be sent func (store *fileStore) SetNextSenderMsgSeqNum(next int) error { - store.cache.SetNextSenderMsgSeqNum(next) - return store.setSeqNum(store.senderSeqNumsFile, next) + if err := store.cache.SetNextSenderMsgSeqNum(next); err != nil { + return errors.Wrap(err, "cache") + } + if err := store.setSeqNum(store.senderSeqNumsFile, next); err != nil { + return err + } + + store.backupStore.SetNextSenderMsgSeqNum(next) + + return nil } // SetNextTargetMsgSeqNum sets the next MsgSeqNum that should be received func (store *fileStore) SetNextTargetMsgSeqNum(next int) error { - store.cache.SetNextTargetMsgSeqNum(next) - return store.setSeqNum(store.targetSeqNumsFile, next) + if err := store.cache.SetNextTargetMsgSeqNum(next); err != nil { + return errors.Wrap(err, "cache") + } + if err := store.setSeqNum(store.targetSeqNumsFile, next); err != nil { + return err + } + + store.backupStore.SetNextTargetMsgSeqNum(next) + + return nil } // IncrNextSenderMsgSeqNum increments the next MsgSeqNum that will be sent func (store *fileStore) IncrNextSenderMsgSeqNum() error { - store.cache.IncrNextSenderMsgSeqNum() - return store.setSeqNum(store.senderSeqNumsFile, store.cache.NextSenderMsgSeqNum()) + if err := store.cache.IncrNextSenderMsgSeqNum(); err != nil { + return errors.Wrap(err, "cache") + } + + seqNum := store.cache.NextSenderMsgSeqNum() + if err := store.setSeqNum(store.senderSeqNumsFile, seqNum); err != nil { + return err + } + + store.backupStore.SetNextSenderMsgSeqNum(seqNum) + + return nil } // IncrNextTargetMsgSeqNum increments the next MsgSeqNum that should be received func (store *fileStore) IncrNextTargetMsgSeqNum() error { - store.cache.IncrNextTargetMsgSeqNum() - return store.setSeqNum(store.targetSeqNumsFile, store.cache.NextTargetMsgSeqNum()) + if err := store.cache.IncrNextTargetMsgSeqNum(); err != nil { + return errors.Wrap(err, "cache") + } + + seqNum := store.cache.NextTargetMsgSeqNum() + if err := store.setSeqNum(store.targetSeqNumsFile, seqNum); err != nil { + return err + } + + store.backupStore.SetNextTargetMsgSeqNum(seqNum) + + return nil } // CreationTime returns the creation time of the store @@ -250,11 +338,11 @@ func (store *fileStore) CreationTime() time.Time { } func (store *fileStore) SaveMessage(seqNum int, msg []byte) error { - offset, err := store.bodyFile.Seek(0, os.SEEK_END) + offset, err := store.bodyFile.Seek(0, io.SeekEnd) if err != nil { return fmt.Errorf("unable to seek to end of file: %s: %s", store.bodyFname, err.Error()) } - if _, err := store.headerFile.Seek(0, os.SEEK_END); err != nil { + if _, err := store.headerFile.Seek(0, io.SeekEnd); err != nil { return fmt.Errorf("unable to seek to end of file: %s: %s", store.headerFname, err.Error()) } if _, err := fmt.Fprintf(store.headerFile, "%d,%d,%d\n", seqNum, offset, len(msg)); err != nil { @@ -266,12 +354,17 @@ func (store *fileStore) SaveMessage(seqNum int, msg []byte) error { if _, err := store.bodyFile.Write(msg); err != nil { return fmt.Errorf("unable to write to file: %s: %s", store.bodyFname, err.Error()) } - if err := store.bodyFile.Sync(); err != nil { - return fmt.Errorf("unable to flush file: %s: %s", store.bodyFname, err.Error()) - } - if err := store.headerFile.Sync(); err != nil { - return fmt.Errorf("unable to flush file: %s: %s", store.headerFname, err.Error()) + if store.fileSync { + if err := store.bodyFile.Sync(); err != nil { + return fmt.Errorf("unable to flush file: %s: %s", store.bodyFname, err.Error()) + } + if err := store.headerFile.Sync(); err != nil { + return fmt.Errorf("unable to flush file: %s: %s", store.headerFname, err.Error()) + } } + + store.backupStore.SaveMessage(seqNum, msg) + return nil } diff --git a/filestore_test.go b/filestore_test.go index 56fc8ec68..74f6d7bbd 100644 --- a/filestore_test.go +++ b/filestore_test.go @@ -35,7 +35,8 @@ TargetCompID=%s`, fileStorePath, sessionID.BeginString, sessionID.SenderCompID, require.Nil(suite.T(), err) // create store - suite.msgStore, err = NewFileStoreFactory(settings).Create(sessionID) + backupStore := &backupStoreFactory{messagesQueue: make(chan *BackupMessage, 100), backupFactory: NewMemoryStoreFactory()} + suite.msgStore, err = NewFileStoreFactory(settings, backupStore).Create(sessionID) require.Nil(suite.T(), err) } diff --git a/fileutil.go b/fileutil.go index 9fa11c964..d470278e5 100644 --- a/fileutil.go +++ b/fileutil.go @@ -4,9 +4,11 @@ import ( "fmt" "os" "strings" + + "github.com/pkg/errors" ) -func sessionIDFilenamePrefix(s SessionID) string { +func SessionIDFilenamePrefix(s SessionID) string { sender := []string{s.SenderCompID} if s.SenderSubID != "" { sender = append(sender, s.SenderSubID) @@ -44,9 +46,8 @@ func closeFile(f *os.File) error { // removeFile behaves like os.Remove, except that no error is returned if the file does not exist func removeFile(fname string) error { - err := os.Remove(fname) - if (err != nil) && !os.IsNotExist(err) { - return err + if err := os.Remove(fname); (err != nil) && !os.IsNotExist(err) { + return errors.Wrapf(err, "remove %v", fname) } return nil } diff --git a/fileutil_test.go b/fileutil_test.go index 4817c7862..a85ace6f7 100644 --- a/fileutil_test.go +++ b/fileutil_test.go @@ -25,7 +25,7 @@ func TestSessionIDFilename_MinimallyQualifiedSessionID(t *testing.T) { sessionID := SessionID{BeginString: "FIX.4.4", SenderCompID: "SENDER", TargetCompID: "TARGET"} // Then the filename should be - require.Equal(t, "FIX.4.4-SENDER-TARGET", sessionIDFilenamePrefix(sessionID)) + require.Equal(t, "FIX.4.4-SENDER-TARGET", SessionIDFilenamePrefix(sessionID)) } func TestSessionIDFilename_FullyQualifiedSessionID(t *testing.T) { @@ -42,7 +42,7 @@ func TestSessionIDFilename_FullyQualifiedSessionID(t *testing.T) { } // Then the filename should be - require.Equal(t, "FIX.4.4-A_B_C-D_E_F-G", sessionIDFilenamePrefix(sessionID)) + require.Equal(t, "FIX.4.4-A_B_C-D_E_F-G", SessionIDFilenamePrefix(sessionID)) } func TestOpenOrCreateFile(t *testing.T) { @@ -53,6 +53,7 @@ func TestOpenOrCreateFile(t *testing.T) { // Then it should be created f, err := openOrCreateFile(fname, 0664) + require.Nil(t, err) requireFileExists(t, fname) // When the file already exists diff --git a/fix_int_test.go b/fix_int_test.go index 173cb5ee5..64142c045 100644 --- a/fix_int_test.go +++ b/fix_int_test.go @@ -32,6 +32,6 @@ func BenchmarkFIXInt_Read(b *testing.B) { var field FIXInt for i := 0; i < b.N; i++ { - field.Read(intBytes) + _ = field.Read(intBytes) } } diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..b8aa1a82f --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module github.com/quickfixgo/quickfix + +go 1.13 + +require ( + github.com/armon/go-proxyproto v0.0.0-20200108142055-f0b8253b1507 + github.com/glebarez/sqlite v1.4.1 + github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 + github.com/kr/pretty v0.3.0 // indirect + github.com/mattn/go-sqlite3 v1.14.10 + github.com/nutsdb/nutsdb v0.12.0 + github.com/pkg/errors v0.9.1 + github.com/shopspring/decimal v1.2.0 + github.com/smartystreets/goconvey v1.6.4 + github.com/stretchr/testify v1.8.0 + golang.org/x/crypto v0.4.0 // indirect + golang.org/x/net v0.3.0 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gorm.io/driver/postgres v1.3.1 + gorm.io/gorm v1.23.3 +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..d0382e947 --- /dev/null +++ b/go.sum @@ -0,0 +1,404 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/armon/go-proxyproto v0.0.0-20200108142055-f0b8253b1507 h1:dmVRVC/MmuwC2edm/P6oWIP+9n+p9IgVgK0lq9mBQjU= +github.com/armon/go-proxyproto v0.0.0-20200108142055-f0b8253b1507/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= +github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= +github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/glebarez/go-sqlite v1.15.1 h1:1gRIcUp1EFZ9wn7qBVFte332R6elCC6nJl4+YWu7SNI= +github.com/glebarez/go-sqlite v1.15.1/go.mod h1:rAfxRB8nJkvpDoj3sCegn4Sm/w4xX3o2lx7GiJ5vs0k= +github.com/glebarez/sqlite v1.4.1 h1:IrFURFnrjiPhRW8D8N7YvUssBNzfbrTb4xN6Mk7W7rM= +github.com/glebarez/sqlite v1.4.1/go.mod h1:OI0VEF6vz0qLnOr3ooLCuVdsxwNrPlo9Bscqjg9x2bM= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8= +github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= +github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.9.1 h1:MJc2s0MFS8C3ok1wQTdQxWuXQcB6+HwAm5x1CzW7mf0= +github.com/jackc/pgtype v1.9.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.14.1 h1:71oo1KAGI6mXhLiTMn6iDFcp3e7+zon/capWjl2OEFU= +github.com/jackc/pgx/v4 v4.14.1/go.mod h1:RgDuE4Z34o7XE92RpLsvFiOEfrAUT0Xt2KxvX73W06M= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk= +github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/nutsdb/nutsdb v0.12.0 h1:6P7EJat2PyVhRu51KMmFu5N851UupfpPBHAqctRm3/4= +github.com/nutsdb/nutsdb v0.12.0/go.mod h1:FSztXVhUSK5YmedmZQ6m37cU/KpVbGaezUEmUBP8DEo= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/xujiajun/gorouter v1.2.0/go.mod h1:yJrIta+bTNpBM/2UT8hLOaEAFckO+m/qmR3luMIQygM= +github.com/xujiajun/mmap-go v1.0.1 h1:7Se7ss1fLPPRW+ePgqGpCkfGIZzJV6JPq9Wq9iv/WHc= +github.com/xujiajun/mmap-go v1.0.1/go.mod h1:CNN6Sw4SL69Sui00p0zEzcZKbt+5HtEnYUsc6BKKRMg= +github.com/xujiajun/utils v0.0.0-20220904132955-5f7c5b914235 h1:w0si+uee0iAaCJO9q86T6yrhdadgcsoNuh47LrUykzg= +github.com/xujiajun/utils v0.0.0-20220904132955-5f7c5b914235/go.mod h1:MR4+0R6A9NS5IABnIM3384FfOq8QFVnm7WDrBOhIaMU= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220405210540-1e041c57c461/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.3.1 h1:Pyv+gg1Gq1IgsLYytj/S2k7ebII3CzEdpqQkPOdH24g= +gorm.io/driver/postgres v1.3.1/go.mod h1:WwvWOuR9unCLpGWCL6Y3JOeBWvbKi6JLhayiVclSZZU= +gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.3 h1:jYh3nm7uLZkrMVfA8WVNjDZryKfr7W+HTlInVgKFJAg= +gorm.io/gorm v1.23.3/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.20/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.22/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.24/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= +modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= +modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI= +modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag= +modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw= +modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ= +modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c= +modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo= +modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg= +modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I= +modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs= +modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8= +modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE= +modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk= +modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w= +modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE= +modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8= +modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc= +modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU= +modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE= +modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk= +modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI= +modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE= +modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg= +modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74= +modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU= +modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU= +modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc= +modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM= +modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ= +modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= +modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= +modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= +modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= +modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU= +modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko= +modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA= +modernc.org/ccgo/v3 v3.13.1/go.mod h1:aBYVOUfIlcSnrsRVU8VRS35y2DIfpgkmVkYZ0tpIXi4= +modernc.org/ccgo/v3 v3.15.9/go.mod h1:md59wBwDT2LznX/OTCPoVS6KIsdRgY8xqQwBV+hkTH0= +modernc.org/ccgo/v3 v3.15.10/go.mod h1:wQKxoFn0ynxMuCLfFD09c8XPUCc8obfchoVR9Cn0fI8= +modernc.org/ccgo/v3 v3.15.12/go.mod h1:VFePOWoCd8uDGRJpq/zfJ29D0EVzMSyID8LCMWYbX6I= +modernc.org/ccgo/v3 v3.15.14/go.mod h1:144Sz2iBCKogb9OKwsu7hQEub3EVgOlyI8wMUPGKUXQ= +modernc.org/ccgo/v3 v3.15.15/go.mod h1:z5qltXjU4PJl0pE5nhYQCvA9DhPHiWsl5GWl89+NSYE= +modernc.org/ccgo/v3 v3.15.16/go.mod h1:XbKRMeMWMdq712Tr5ECgATYMrzJ+g9zAZEj2ktzBe24= +modernc.org/ccgo/v3 v3.15.17/go.mod h1:bofnFkpRFf5gLY+mBZIyTW6FEcp26xi2lgOFk2Rlvs0= +modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= +modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg= +modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M= +modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU= +modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE= +modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso= +modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8= +modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8= +modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I= +modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk= +modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY= +modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE= +modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg= +modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM= +modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg= +modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo= +modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8= +modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ= +modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA= +modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM= +modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg= +modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE= +modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM= +modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU= +modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw= +modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M= +modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18= +modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8= +modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= +modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= +modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= +modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= +modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= +modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ= +modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= +modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= +modernc.org/libc v1.12.0/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ= +modernc.org/libc v1.14.1/go.mod h1:npFeGWjmZTjFeWALQLrvklVmAxv4m80jnG3+xI8FdJk= +modernc.org/libc v1.14.2/go.mod h1:MX1GBLnRLNdvmK9azU9LCxZ5lMyhrbEMK8rG3X/Fe34= +modernc.org/libc v1.14.3/go.mod h1:GPIvQVOVPizzlqyRX3l756/3ppsAgg1QgPxjr5Q4agQ= +modernc.org/libc v1.14.6/go.mod h1:2PJHINagVxO4QW/5OQdRrvMYo+bm5ClpUFfyXCYl9ak= +modernc.org/libc v1.14.7/go.mod h1:f8xfWXW8LW41qb4X5+huVQo5dcfPlq7Cbny2TDheMv0= +modernc.org/libc v1.14.8/go.mod h1:9+JCLb1MWSY23smyOpIPbd5ED+rSS/ieiDWUpdyO3mo= +modernc.org/libc v1.14.10/go.mod h1:y1MtIWhwpJFpLYm6grAThtuXJKEsY6xkdZmXbRngIdo= +modernc.org/libc v1.14.11/go.mod h1:l5/Mz/GrZwOqzwRHA3abgSCnSeJzzTl+Ify0bAwKbAw= +modernc.org/libc v1.14.12 h1:pUBZTYoISfbb4pCf4PECENpbvwDBxeKc+/dS9LyOWFM= +modernc.org/libc v1.14.12/go.mod h1:fJdoe23MHu2ruPQkFPPqCpToDi5cckzsbmkI6Ez0LqQ= +modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= +modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= +modernc.org/memory v1.0.6/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.0.7 h1:UE3cxTRFa5tfUibAV7Jqq8P7zRY0OlJg+yWVIIaluEE= +modernc.org/memory v1.0.7/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.15.2 h1:Es0SrEJUQHH7rt6uC/Zh2gHQ0AUhgB+F2RQqpXf3MNs= +modernc.org/sqlite v1.15.2/go.mod h1:2P9bWfawhYMpYsBELqKREE+LFZo4uPApOuqszlZ7QX8= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/tcl v1.11.2/go.mod h1:BRzgpajcGdS2qTxniOx9c/dcxjlbA7p12eJNmiriQYo= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.3.2/go.mod h1:PEU2oK2OEA1CfzDTd+8E908qEXhC9s0MfyKp5LZsd+k= diff --git a/gormstore_model.go b/gormstore_model.go new file mode 100644 index 000000000..fffcbf5a6 --- /dev/null +++ b/gormstore_model.go @@ -0,0 +1,40 @@ +package quickfix + +import ( + "time" +) + +type GormSessions struct { + BeginString string `gorm:"column:beginstring;primaryKey;type:varchar(8)"` + SenderCompId string `gorm:"column:sendercompid;primaryKey;type:varchar(64)"` + SenderSubId string `gorm:"column:sendersubid;primaryKey;type:varchar(64)"` + SenderLocId string `gorm:"column:senderlocid;primaryKey;type:varchar(64)"` + TargetCompId string `gorm:"column:targetcompid;primaryKey;type:varchar(64)"` + TargetSubId string `gorm:"column:targetsubid;primaryKey;type:varchar(64)"` + TargetLocId string `gorm:"column:targetlocid;primaryKey;type:varchar(64)"` + SessionQualifier string `gorm:"column:session_qualifier;primaryKey;type:varchar(64)"` + CreationTime time.Time `gorm:"column:creation_time"` + IncomingSeqNum int `gorm:"column:incoming_seqnum"` + OutgoingSeqNum int `gorm:"column:outgoing_seqnum"` +} + +func (g GormSessions) TableName() string { + return "sessions" +} + +type GormMessages struct { + BeginString string `gorm:"column:beginstring;primaryKey;type:varchar(8)"` + SenderCompId string `gorm:"column:sendercompid;primaryKey;type:varchar(64)"` + SenderSubId string `gorm:"column:sendersubid;primaryKey;type:varchar(64)"` + SenderLocId string `gorm:"column:senderlocid;primaryKey;type:varchar(64)"` + TargetCompId string `gorm:"column:targetcompid;primaryKey;type:varchar(64)"` + TargetSubId string `gorm:"column:targetsubid;primaryKey;type:varchar(64)"` + TargetLocId string `gorm:"column:targetlocid;primaryKey;type:varchar(64)"` + SessionQualifier string `gorm:"column:session_qualifier;primaryKey;type:varchar(64)"` + Message string `gorm:"column:message;type:text"` + MsgSeqNum int64 `gorm:"column:msgseqnum;primaryKey"` +} + +func (g GormMessages) TableName() string { + return "messages" +} diff --git a/gormstroe.go b/gormstroe.go new file mode 100644 index 000000000..2e6c9a59a --- /dev/null +++ b/gormstroe.go @@ -0,0 +1,275 @@ +package quickfix + +import ( + "fmt" + "time" + + "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/quickfixgo/quickfix/config" +) + +type gormStoreFactory struct { + settings *Settings + db *gorm.DB +} + +func NewGormStoreFactory(settings *Settings, db *gorm.DB) MessageStoreFactory { + return gormStoreFactory{settings: settings, db: db} +} + +type gromStore struct { + sessionID SessionID + cache *memoryStore + db *gorm.DB +} + +func (f gormStoreFactory) Create(sessionID SessionID) (msgStore MessageStore, err error) { + var dynamicSessions bool + if f.settings.GlobalSettings().HasSetting(config.DynamicSessions) { + if dynamicSessions, err = f.settings.globalSettings.BoolSetting(config.DynamicSessions); err != nil { + return + } + } + _, ok := f.settings.SessionSettings()[sessionID] + if !ok && !dynamicSessions { + return nil, fmt.Errorf("unknown session: %v", sessionID) + } + + store := &gromStore{ + sessionID: sessionID, + cache: &memoryStore{}, + db: f.db, + } + err = store.initTables() + if err != nil { + err = errors.Wrap(err, "initTables err") + return + } + if err = store.cache.Reset(); err != nil { + err = errors.Wrap(err, "cache reset") + return + } + if err = store.populateCache(); err != nil { + return nil, err + } + return store, nil + +} + +func (store *gromStore) initTables() (err error) { + if !store.db.Migrator().HasTable("sessions") { + err = store.db.Migrator().CreateTable(&GormSessions{}) + if err != nil { + return errors.Wrap(err, "gromStore.initTables err") + } + } + if !store.db.Migrator().HasTable("messages") { + err = store.db.Migrator().CreateTable(&GormMessages{}) + if err != nil { + return errors.Wrap(err, "gromStore.initTables err") + } + } + return nil +} + +// Reset deletes the store records and sets the seqnums back to 1 +func (store *gromStore) Reset() error { + s := store.sessionID + err := store.db.Exec(`DELETE FROM messages + WHERE beginstring=? AND session_qualifier=? + AND sendercompid=? AND sendersubid=? AND senderlocid=? + AND targetcompid=? AND targetsubid=? AND targetlocid=?`, s.BeginString, s.Qualifier, + s.SenderCompID, s.SenderSubID, s.SenderLocationID, + s.TargetCompID, s.TargetSubID, s.TargetLocationID).Error + if err != nil { + return err + } + if err = store.cache.Reset(); err != nil { + return err + } + err = store.db.Table(`sessions`).Where(`beginstring=? AND session_qualifier=? + AND sendercompid=? AND sendersubid=? AND senderlocid=? + AND targetcompid=? AND targetsubid=? AND targetlocid=?`, s.BeginString, s.Qualifier, + s.SenderCompID, s.SenderSubID, s.SenderLocationID, + s.TargetCompID, s.TargetSubID, s.TargetLocationID).Updates(map[string]interface{}{ + "creation_time": store.cache.CreationTime(), + "incoming_seqnum": store.cache.NextTargetMsgSeqNum(), + "outgoing_seqnum": store.cache.NextSenderMsgSeqNum(), + }).Error + return err +} + +// Refresh reloads the store from the database +func (store *gromStore) Refresh() error { + if err := store.cache.Reset(); err != nil { + return err + } + return store.populateCache() +} + +func (store *gromStore) populateCache() error { + dest := GormSessions{} + s := store.sessionID + err := store.db.Table(`sessions`).Where(`beginstring=? AND session_qualifier=? + AND sendercompid=? AND sendersubid=? AND senderlocid=? + AND targetcompid=? AND targetsubid=? AND targetlocid=?`, s.BeginString, s.Qualifier, + s.SenderCompID, s.SenderSubID, s.SenderLocationID, + s.TargetCompID, s.TargetSubID, s.TargetLocationID).First(&dest).Error + if err == nil { + store.cache.creationTime = dest.CreationTime + if err = store.cache.SetNextTargetMsgSeqNum(dest.IncomingSeqNum); err != nil { + return errors.Wrap(err, "cache set next target") + } + if err = store.cache.SetNextSenderMsgSeqNum(dest.OutgoingSeqNum); err != nil { + return errors.Wrap(err, "cache set next sender") + } + return nil + } + if err == gorm.ErrRecordNotFound { + return store.db.Exec(`INSERT INTO sessions ( + creation_time, incoming_seqnum, outgoing_seqnum, + beginstring, session_qualifier, + sendercompid, sendersubid, senderlocid, + targetcompid, targetsubid, targetlocid) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, store.cache.creationTime, + store.cache.NextTargetMsgSeqNum(), + store.cache.NextSenderMsgSeqNum(), + s.BeginString, s.Qualifier, + s.SenderCompID, s.SenderSubID, s.SenderLocationID, + s.TargetCompID, s.TargetSubID, s.TargetLocationID).Error + } + return err +} + +// NextSenderMsgSeqNum returns the next MsgSeqNum that will be sent +func (store *gromStore) NextSenderMsgSeqNum() int { + return store.cache.NextSenderMsgSeqNum() +} + +// NextTargetMsgSeqNum returns the next MsgSeqNum that should be received +func (store *gromStore) NextTargetMsgSeqNum() int { + return store.cache.NextTargetMsgSeqNum() +} + +// SetNextSenderMsgSeqNum sets the next MsgSeqNum that will be sent +func (store *gromStore) SetNextSenderMsgSeqNum(next int) error { + s := store.sessionID + + err := store.db.Table(`sessions`).Where(`beginstring=? AND session_qualifier=? + AND sendercompid=? AND sendersubid=? AND senderlocid=? + AND targetcompid=? AND targetsubid=? AND targetlocid=?`, s.BeginString, s.Qualifier, + s.SenderCompID, s.SenderSubID, s.SenderLocationID, + s.TargetCompID, s.TargetSubID, s.TargetLocationID).Update(`outgoing_seqnum`, next).Error + if err != nil { + return err + } + return store.cache.SetNextSenderMsgSeqNum(next) +} + +// SetNextTargetMsgSeqNum sets the next MsgSeqNum that should be received +func (store *gromStore) SetNextTargetMsgSeqNum(next int) error { + s := store.sessionID + + err := store.db.Table(`sessions`).Where(`beginstring=? AND session_qualifier=? + AND sendercompid=? AND sendersubid=? AND senderlocid=? + AND targetcompid=? AND targetsubid=? AND targetlocid=?`, s.BeginString, s.Qualifier, + s.SenderCompID, s.SenderSubID, s.SenderLocationID, + s.TargetCompID, s.TargetSubID, s.TargetLocationID).Update(`incoming_seqnum`, next).Error + if err != nil { + return err + } + return store.cache.SetNextTargetMsgSeqNum(next) +} + +// IncrNextSenderMsgSeqNum increments the next MsgSeqNum that will be sent +func (store *gromStore) IncrNextSenderMsgSeqNum() error { + if err := store.cache.IncrNextSenderMsgSeqNum(); err != nil { + return errors.Wrap(err, "cache incr next") + } + return store.SetNextSenderMsgSeqNum(store.cache.NextSenderMsgSeqNum()) +} + +// IncrNextTargetMsgSeqNum increments the next MsgSeqNum that should be received +func (store *gromStore) IncrNextTargetMsgSeqNum() error { + if err := store.cache.IncrNextTargetMsgSeqNum(); err != nil { + return errors.Wrap(err, "cache incr next") + } + return store.SetNextTargetMsgSeqNum(store.cache.NextTargetMsgSeqNum()) +} + +// CreationTime returns the creation time of the store +func (store *gromStore) CreationTime() time.Time { + return store.cache.CreationTime() +} + +func (store *gromStore) SaveMessage(seqNum int, msg []byte) error { + s := store.sessionID + err := store.db.Exec(`INSERT INTO messages ( + msgseqnum, message, + beginstring, session_qualifier, + sendercompid, sendersubid, senderlocid, + targetcompid, targetsubid, targetlocid) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, seqNum, string(msg), + s.BeginString, s.Qualifier, + s.SenderCompID, s.SenderSubID, s.SenderLocationID, + s.TargetCompID, s.TargetSubID, s.TargetLocationID).Error + if err != nil { + var counter int64 + store.db.Table("messages").Where(`beginstring=? AND session_qualifier=? + AND sendercompid=? AND sendersubid=? AND senderlocid=? + AND targetcompid=? AND targetsubid=? AND targetlocid=? + AND msgseqnum=?`, s.BeginString, s.Qualifier, + s.SenderCompID, s.SenderSubID, s.SenderLocationID, + s.TargetCompID, s.TargetSubID, s.TargetLocationID, + seqNum).Limit(1).Count(&counter) + //If it is determined that the message is repeated, skip this insertion + if counter == 1 { + return nil + } + } + return err +} + +func (store *gromStore) GetMessages(beginSeqNum, endSeqNum int) ([][]byte, error) { + s := store.sessionID + var msgs [][]byte + rows, err := store.db.Raw(`SELECT message FROM messages + WHERE beginstring=? AND session_qualifier=? + AND sendercompid=? AND sendersubid=? AND senderlocid=? + AND targetcompid=? AND targetsubid=? AND targetlocid=? + AND msgseqnum>=? AND msgseqnum<=? + ORDER BY msgseqnum`, s.BeginString, s.Qualifier, + s.SenderCompID, s.SenderSubID, s.SenderLocationID, + s.TargetCompID, s.TargetSubID, s.TargetLocationID, + beginSeqNum, endSeqNum).Rows() + + if err != nil { + return nil, err + } + for rows.Next() { + var message string + if err := rows.Scan(&message); err != nil { + return nil, err + } + msgs = append(msgs, []byte(message)) + } + if err := rows.Err(); err != nil { + return nil, err + } + return msgs, nil + +} + +// Close closes the store's database connection +func (store *gromStore) Close() error { + if store.db != nil { + db, err := store.db.DB() + if err != nil { + db.Close() + } + store.db = nil + } + return nil +} diff --git a/gormstroe_test.go b/gormstroe_test.go new file mode 100644 index 000000000..0065f9a8f --- /dev/null +++ b/gormstroe_test.go @@ -0,0 +1,89 @@ +package quickfix + +import ( + "testing" + + "github.com/glebarez/sqlite" + . "github.com/smartystreets/goconvey/convey" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func NewGormDB() (*gorm.DB, error) { + dsn := "host=127.0.0.1 user=postgres dbname=lb_test port=5432 sslmode=disable TimeZone=Asia/Shanghai" + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err == nil { + db.Migrator().DropTable(&GormSessions{}) + } + return db, err +} + +func NewGormSqliteDB() (db *gorm.DB, err error) { + db, err = gorm.Open(sqlite.Open("test.db?_pragma=busy_timeout(500000)"), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) + sqldb, _ := db.DB() + sqldb.SetMaxOpenConns(1) + return +} + +func Test_GromStoreCreate(t *testing.T) { + Convey(`GromStoreCreate`, t, func() { + db, err := NewGormDB() + So(err, ShouldBeNil) + So(db, ShouldNotBeNil) + sessionID := SessionID{BeginString: "FIX.4.2", TargetCompID: "IB", SenderCompID: "LB"} + appSettings := NewSettings() + appSettings.sessionSettings = map[SessionID]*SessionSettings{ + sessionID: {}, + } + Convey(`none session`, func() { + + }) + Convey(`have session`, func() { + f := NewGormStoreFactory(appSettings, db) + f.Create(sessionID) + }) + }) +} + +func Test_GromStoreSaveMessage(t *testing.T) { + Convey(`GromStoreSaveMessage`, t, func() { + db, err := NewGormDB() + So(err, ShouldBeNil) + So(db, ShouldNotBeNil) + sessionID := SessionID{BeginString: "FIX.4.2", TargetCompID: "IB", SenderCompID: "LB"} + appSettings := NewSettings() + appSettings.sessionSettings = map[SessionID]*SessionSettings{ + sessionID: {}, + } + Convey(`duplicate key`, func() { + f := NewGormStoreFactory(appSettings, db) + store, err := f.Create(sessionID) + So(err, ShouldBeNil) + err = store.SaveMessage(1, []byte(`test msg`)) + err = store.SaveMessage(1, []byte(`test msg`)) + So(err, ShouldBeNil) + }) + }) +} + +func Test_SetNextSenderMsgSeqNum(t *testing.T) { + Convey(`SetNextSenderMsgSeqNum`, t, func() { + db, err := NewGormSqliteDB() + So(err, ShouldBeNil) + So(db, ShouldNotBeNil) + sessionID := SessionID{BeginString: "FIX.4.2", TargetCompID: "IB", SenderCompID: "LB"} + sessionID = SessionID{} + appSettings := NewSettings() + appSettings.sessionSettings = map[SessionID]*SessionSettings{ + sessionID: {}, + } + f := NewGormStoreFactory(appSettings, db) + store, err := f.Create(sessionID) + So(err, ShouldBeNil) + err = store.SetNextSenderMsgSeqNum(10) + So(err, ShouldBeNil) + }) +} diff --git a/in_session.go b/in_session.go index d4dfc701d..b33becb5a 100644 --- a/in_session.go +++ b/in_session.go @@ -9,7 +9,7 @@ import ( type inSession struct{ loggedOn } -func (state inSession) String() string { return "In Session" } +func (state inSession) String() string { return SessionStateInSession } func (state inSession) FixMsgIn(session *session, msg *Message) sessionState { msgType, err := msg.Header.GetBytes(tagMsgType) @@ -238,8 +238,9 @@ func (state inSession) resendMessages(session *session, beginSeqNo, endSeqNo int } session.log.OnEventf("Resending Message: %v", sentMessageSeqNum) - msgBytes = msg.build() - session.sendBytes(msgBytes) + + msgBytes = msg.buildWithBodyBytes(msg.bodyBytes) // workaround for maintaining repeating group field order + session.EnqueueBytesAndSend(msgBytes) seqNum = sentMessageSeqNum + 1 nextSeqNum = seqNum @@ -382,7 +383,7 @@ func (state *inSession) generateSequenceReset(session *session, beginSeqNo int, msgBytes := sequenceReset.build() - session.sendBytes(msgBytes) + session.EnqueueBytesAndSend(msgBytes) session.log.OnEventf("Sent SequenceReset TO: %v", endSeqNo) return diff --git a/initiator.go b/initiator.go index 21317d76d..8d047a00c 100644 --- a/initiator.go +++ b/initiator.go @@ -3,12 +3,14 @@ package quickfix import ( "bufio" "crypto/tls" - "net" + "strings" "sync" "time" + + "golang.org/x/net/proxy" ) -//Initiator initiates connections and processes messages for all sessions. +// Initiator initiates connections and processes messages for all sessions. type Initiator struct { app Application settings *Settings @@ -22,7 +24,7 @@ type Initiator struct { sessionFactory } -//Start Initiator. +// Start Initiator. func (i *Initiator) Start() (err error) { i.stopChan = make(chan interface{}) @@ -33,9 +35,14 @@ func (i *Initiator) Start() (err error) { return } + var dialer proxy.Dialer + if dialer, err = loadDialerConfig(settings); err != nil { + return + } + i.wg.Add(1) go func(sessID SessionID) { - i.handleConnection(i.sessions[sessID], tlsConfig) + i.handleConnection(i.sessions[sessID], tlsConfig, dialer) i.wg.Done() }(sessionID) } @@ -43,13 +50,37 @@ func (i *Initiator) Start() (err error) { return } -//Stop Initiator. +// Stop Initiator. func (i *Initiator) Stop() { + select { + case <-i.stopChan: + //closed already + return + default: + } close(i.stopChan) i.wg.Wait() } -//NewInitiator creates and initializes a new Initiator. +func (i *Initiator) IsConnectedAndLoggedOn(sessionID SessionID) bool { + session, ok := i.sessions[sessionID] + if !ok { + return false + } + + return session.IsConnected() && session.IsLoggedOn() +} + +func (i *Initiator) SessionState(sessionID SessionID) string { + session, ok := i.sessions[sessionID] + if !ok { + return SessionStateUnknown + } + + return session.State.String() +} + +// NewInitiator creates and initializes a new Initiator. func NewInitiator(app Application, storeFactory MessageStoreFactory, appSettings *Settings, logFactory LogFactory) (*Initiator, error) { i := &Initiator{ app: app, @@ -79,7 +110,7 @@ func NewInitiator(app Application, storeFactory MessageStoreFactory, appSettings return i, nil } -//waitForInSessionTime returns true if the session is in session, false if the handler should stop +// waitForInSessionTime returns true if the session is in session, false if the handler should stop func (i *Initiator) waitForInSessionTime(session *session) bool { inSessionTime := make(chan interface{}) go func() { @@ -96,7 +127,7 @@ func (i *Initiator) waitForInSessionTime(session *session) bool { return true } -//watiForReconnectInterval returns true if a reconnect should be re-attempted, false if handler should stop +// waitForReconnectInterval returns true if a reconnect should be re-attempted, false if handler should stop func (i *Initiator) waitForReconnectInterval(reconnectInterval time.Duration) bool { select { case <-time.After(reconnectInterval): @@ -107,7 +138,7 @@ func (i *Initiator) waitForReconnectInterval(reconnectInterval time.Duration) bo return true } -func (i *Initiator) handleConnection(session *session, tlsConfig *tls.Config) { +func (i *Initiator) handleConnection(session *session, tlsConfig *tls.Config, dialer proxy.Dialer) { var wg sync.WaitGroup wg.Add(1) go func() { @@ -121,6 +152,7 @@ func (i *Initiator) handleConnection(session *session, tlsConfig *tls.Config) { }() connectionAttempt := 0 + useLastLogon := session.lastLogonData != nil for { if !i.waitForInSessionTime(session) { @@ -130,31 +162,33 @@ func (i *Initiator) handleConnection(session *session, tlsConfig *tls.Config) { var disconnected chan interface{} var msgIn chan fixIn var msgOut chan []byte - address := session.SocketConnectAddress[connectionAttempt%len(session.SocketConnectAddress)] - session.log.OnEventf("Connecting to: %v", address) + if useLastLogon && session.lastLogonData != nil { + address = session.lastLogonData.Addr + } + session.log.OnEventf("Session: %+v Connecting to: %v, useLastLogon: %v", session.sessionID, address, useLastLogon) + session.lastConnectData = &EventLogon{Addr: address, TS: time.Now().Unix()} - var netConn net.Conn - if tlsConfig != nil { - tlsConn, err := tls.Dial("tcp", address, tlsConfig) - if err != nil { - session.log.OnEventf("Failed to connect: %v", err) - goto reconnect + netConn, err := dialer.Dial("tcp", address) + if err != nil { + session.log.OnEventf("Failed to connect: %v", err) + goto reconnect + } else if tlsConfig != nil { + // Unless InsecureSkipVerify is true, server name config is required for TLS + // to verify the received certificate + if !tlsConfig.InsecureSkipVerify && len(tlsConfig.ServerName) == 0 { + serverName := address + if c := strings.LastIndex(serverName, ":"); c > 0 { + serverName = serverName[:c] + } + tlsConfig.ServerName = serverName } - - err = tlsConn.Handshake() - if err != nil { - session.log.OnEventf("Failed handshake:%v", err) + tlsConn := tls.Client(netConn, tlsConfig) + if err = tlsConn.Handshake(); err != nil { + session.log.OnEventf("Failed handshake: %v", err) goto reconnect } netConn = tlsConn - } else { - var err error - netConn, err = net.Dial("tcp", address) - if err != nil { - session.log.OnEventf("Failed to connect: %v", err) - goto reconnect - } } msgIn = make(chan fixIn) @@ -181,7 +215,13 @@ func (i *Initiator) handleConnection(session *session, tlsConfig *tls.Config) { } reconnect: - connectionAttempt++ + if !useLastLogon { + connectionAttempt++ + } + if session.lastLogonData != nil { + useLastLogon = !useLastLogon + } + session.log.OnEventf("Reconnecting in %v", session.ReconnectInterval) if !i.waitForReconnectInterval(session.ReconnectInterval) { return diff --git a/internal/event_timer.go b/internal/event_timer.go index 2913684c5..0b7630cbc 100644 --- a/internal/event_timer.go +++ b/internal/event_timer.go @@ -8,38 +8,31 @@ import ( type EventTimer struct { f func() timer *time.Timer - reset chan time.Duration + done chan struct{} wg sync.WaitGroup } func NewEventTimer(task func()) *EventTimer { t := &EventTimer{ f: task, - reset: make(chan time.Duration, 1), + timer: newStoppedTimer(), + done: make(chan struct{}), } t.wg.Add(1) go func() { defer t.wg.Done() - var c <-chan time.Time for { select { - case <-c: + case <-t.timer.C: t.f() - case d, ok := <-t.reset: - if !ok { - return - } + case <-t.done: + t.timer.Stop() + return - if t.timer != nil { - t.timer.Reset(d) - } else { - t.timer = time.NewTimer(d) - c = t.timer.C - } } } }() @@ -52,7 +45,7 @@ func (t *EventTimer) Stop() { return } - close(t.reset) + close(t.done) t.wg.Wait() } @@ -61,5 +54,13 @@ func (t *EventTimer) Reset(timeout time.Duration) { return } - t.reset <- timeout + t.timer.Reset(timeout) +} + +func newStoppedTimer() *time.Timer { + timer := time.NewTimer(time.Second) + if !timer.Stop() { + <-timer.C + } + return timer } diff --git a/internal/rate_limit.go b/internal/rate_limit.go new file mode 100644 index 000000000..5b867bf02 --- /dev/null +++ b/internal/rate_limit.go @@ -0,0 +1,67 @@ +package internal + +import ( + "container/list" + "sync" + "time" +) + +type token struct { + enableTime time.Time +} + +type LimitBucket struct { + fillInterval time.Duration + queue *list.List + mutex sync.Mutex +} + +func New(fillInterval time.Duration, limit uint64) *LimitBucket { + bucket := &LimitBucket{ + fillInterval: fillInterval, + queue: list.New(), + } + now := time.Now() + for i := 0; i < int(limit); i++ { + bucket.queue.PushBack(&token{ + enableTime: now, + }) + } + return bucket +} + +func (bucket *LimitBucket) Wait() { + for { + if bucket.tryTakeToken() { + return + } + } +} + +func (bucket *LimitBucket) WaitForTimeout(timeout time.Duration) { + begin := time.Now() + for { + if time.Now().After(begin.Add(timeout)) { + return + } + if bucket.tryTakeToken() { + return + } + } +} + +func (bucket *LimitBucket) tryTakeToken() bool { + bucket.mutex.Lock() + defer bucket.mutex.Unlock() + front := bucket.queue.Front() + fristToken := front.Value.(*token) + now := time.Now() + if now.After(fristToken.enableTime) { + bucket.queue.Remove(front) + bucket.queue.PushBack(&token{ + enableTime: now.Add(bucket.fillInterval), + }) + return true + } + return false +} diff --git a/internal/ratelimit_test.go b/internal/ratelimit_test.go new file mode 100644 index 000000000..db3ee0864 --- /dev/null +++ b/internal/ratelimit_test.go @@ -0,0 +1,103 @@ +package internal + +import ( + "fmt" + "math/rand" + "testing" + "time" +) + +func TestWaitNormal(t *testing.T) { + bucket := New(time.Second*1, 2) + + for i := 0; i < 10; i++ { + bucket.Wait() + t.Log(time.Now(), "|", i) + } +} + +func TestWaitForTimeout(t *testing.T) { + bucket := New(time.Second*3, 3) + + for i := 0; i < 10; i++ { + bucket.WaitForTimeout(time.Second) + t.Log(time.Now(), "|", i) + } + + for i := 0; i < 10; i++ { + bucket.WaitForTimeout(time.Second * 5) + t.Log(time.Now(), "|", i) + } +} + +func TestWaitRandom(t *testing.T) { + bucket := New(time.Second*1, 3) + for i := 0; i < 10; i++ { + bucket.Wait() + time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000))) + t.Log(time.Now(), "|", i) + } +} + +// func TestWaitConcurrenyOld(t *testing.T) { +// bucket := NewRateLimiter(8) + +// for i := 0; i < 5; i++ { +// bucket.WaitRateLimit() +// t.Log("1:", time.Now(), "|", i) +// } +// time.Sleep(time.Millisecond * 900) +// go func() { +// for i := 0; i < 55; i++ { +// bucket.WaitRateLimit() +// t.Log("2:", time.Now(), "|", i) +// } +// }() + +// time.Sleep(time.Second * 30) +// } + +func TestWaitConcurreny1(t *testing.T) { + bucket := New(time.Second, 8) + + for i := 0; i < 5; i++ { + bucket.Wait() + t.Log("1:", time.Now(), "|", i) + } + time.Sleep(time.Millisecond * 900) + go func() { + for i := 0; i < 55; i++ { + bucket.Wait() + t.Log("1:", time.Now(), "|", i) + } + }() + + time.Sleep(time.Second * 30) +} + +func write(messageOut chan string) { + for { + text := <-messageOut + fmt.Println(time.Now(), text) + time.Sleep(time.Millisecond * time.Duration(100)) + } +} + +func TestWaitConcurreny2(t *testing.T) { + bucket := New(time.Second*2, 5) + messageOut := make(chan string) + go write(messageOut) + go func() { + for i := 0; i < 20; i++ { + bucket.Wait() + messageOut <- fmt.Sprintf("No.1 Worker, task:%v", i) + } + }() + go func() { + for i := 0; i < 20; i++ { + bucket.Wait() + messageOut <- fmt.Sprintf("No.2 Worker, task:%v", i) + } + }() + time.Sleep(time.Second * 30) +} diff --git a/internal/session_settings.go b/internal/session_settings.go index f5d7f9a35..6c91ec20d 100644 --- a/internal/session_settings.go +++ b/internal/session_settings.go @@ -22,5 +22,11 @@ type SessionSettings struct { //specific to initiators ReconnectInterval time.Duration + LogoutTimeout time.Duration + LogonTimeout time.Duration SocketConnectAddress []string + + //rate limt + //SendRateLimiter *RateLimiter + LimitBucket *LimitBucket } diff --git a/internal/time_range_test.go b/internal/time_range_test.go index df392118a..a28a495f5 100644 --- a/internal/time_range_test.go +++ b/internal/time_range_test.go @@ -374,6 +374,7 @@ func TestTimeRangeIsInSameRangeWithDay(t *testing.T) { time1 = time.Date(2004, time.July, 27, 3, 0, 0, 0, time.UTC) time2 = time.Date(2004, time.July, 27, 3, 0, 0, 0, time.UTC) + assert.True(t, NewUTCWeekRange(startTime, endTime, startDay, endDay).IsInSameRange(time1, time2)) time1 = time.Date(2004, time.July, 26, 10, 0, 0, 0, time.UTC) time2 = time.Date(2004, time.July, 27, 3, 0, 0, 0, time.UTC) diff --git a/latent_state.go b/latent_state.go index e6e5bece4..8d35cebf6 100644 --- a/latent_state.go +++ b/latent_state.go @@ -4,7 +4,7 @@ import "github.com/quickfixgo/quickfix/internal" type latentState struct{ inSessionTime } -func (state latentState) String() string { return "Latent State" } +func (state latentState) String() string { return SessionStateLatentState } func (state latentState) IsLoggedOn() bool { return false } func (state latentState) IsConnected() bool { return false } diff --git a/logon_state.go b/logon_state.go index fc7fffd9e..ae92fbcf9 100644 --- a/logon_state.go +++ b/logon_state.go @@ -8,13 +8,14 @@ import ( type logonState struct{ connectedNotLoggedOn } -func (s logonState) String() string { return "Logon State" } +func (s logonState) String() string { return SessionStateLogonState } func (s logonState) FixMsgIn(session *session, msg *Message) (nextState sessionState) { msgType, err := msg.Header.GetBytes(tagMsgType) if err != nil { return handleStateError(session, err) } + session.application.FromAdmin(msg, session.sessionID) if !bytes.Equal(msgType, msgTypeLogon) { session.log.OnEventf("Invalid Session State: Received Msg %s while waiting for Logon", msg) @@ -24,23 +25,15 @@ func (s logonState) FixMsgIn(session *session, msg *Message) (nextState sessionS if err := session.handleLogon(msg); err != nil { switch err := err.(type) { case RejectLogon: - session.log.OnEvent(err.Text) - logout := session.buildLogout(err.Text) + return shutdownWithReason(session, msg, true, err.Error()) - if err := session.dropAndSendInReplyTo(logout, false, msg); err != nil { - session.logError(err) - } - - if err := session.store.IncrNextTargetMsgSeqNum(); err != nil { - session.logError(err) - } - - return latentState{} + case targetTooLow: + return shutdownWithReason(session, msg, false, err.Error()) case targetTooHigh: var tooHighErr error if nextState, tooHighErr = session.doTargetTooHigh(err); tooHighErr != nil { - return handleStateError(session, tooHighErr) + return shutdownWithReason(session, msg, false, tooHighErr.Error()) } return @@ -64,3 +57,20 @@ func (s logonState) Timeout(session *session, e internal.Event) (nextState sessi func (s logonState) Stop(session *session) (nextState sessionState) { return latentState{} } + +func shutdownWithReason(session *session, msg *Message, incrNextTargetMsgSeqNum bool, reason string) (nextState sessionState) { + session.log.OnEvent(reason) + logout := session.buildLogout(reason) + + if err := session.dropAndSendInReplyTo(logout, msg); err != nil { + session.logError(err) + } + + if incrNextTargetMsgSeqNum { + if err := session.store.IncrNextTargetMsgSeqNum(); err != nil { + session.logError(err) + } + } + + return latentState{} +} diff --git a/logon_state_test.go b/logon_state_test.go index 08da2e710..3ba29af1d 100644 --- a/logon_state_test.go +++ b/logon_state_test.go @@ -302,3 +302,31 @@ func (s *LogonStateTestSuite) TestFixMsgInLogonSeqNumTooHigh() { s.State(inSession{}) s.NextTargetMsgSeqNum(7) } + +func (s *LogonStateTestSuite) TestFixMsgInLogonSeqNumTooLow() { + s.IncrNextSenderMsgSeqNum() + s.IncrNextTargetMsgSeqNum() + + logon := s.Logon() + logon.Body.SetField(tagHeartBtInt, FIXInt(32)) + logon.Header.SetInt(tagMsgSeqNum, 1) + + s.MockApp.On("ToAdmin") + s.NextTargetMsgSeqNum(2) + s.fixMsgIn(s.session, logon) + + s.State(latentState{}) + s.NextTargetMsgSeqNum(2) + + s.MockApp.AssertNumberOfCalls(s.T(), "ToAdmin", 1) + msgBytesSent, ok := s.Receiver.LastMessage() + s.Require().True(ok) + sentMessage := NewMessage() + err := ParseMessage(sentMessage, bytes.NewBuffer(msgBytesSent)) + s.Require().Nil(err) + s.MessageType(string(msgTypeLogout), sentMessage) + + s.session.sendQueued() + s.MessageType(string(msgTypeLogout), s.MockApp.lastToAdmin) + s.FieldEquals(tagText, "MsgSeqNum too low, expecting 2 but received 1", s.MockApp.lastToAdmin.Body) +} diff --git a/logout_state.go b/logout_state.go index 071512ce1..0383ff702 100644 --- a/logout_state.go +++ b/logout_state.go @@ -4,7 +4,7 @@ import "github.com/quickfixgo/quickfix/internal" type logoutState struct{ connectedNotLoggedOn } -func (state logoutState) String() string { return "Logout State" } +func (state logoutState) String() string { return SessionStateLogoutState } func (state logoutState) FixMsgIn(session *session, msg *Message) (nextState sessionState) { nextState = inSession{}.FixMsgIn(session, msg) diff --git a/message.go b/message.go index 8d67c4736..078f23d0b 100644 --- a/message.go +++ b/message.go @@ -114,6 +114,22 @@ func NewMessage() *Message { return m } +// CopyInto erases the dest messages and copies the curreny message content +// into it. +func (m *Message) CopyInto(to *Message) { + m.Header.CopyInto(&to.Header.FieldMap) + m.Body.CopyInto(&to.Body.FieldMap) + m.Trailer.CopyInto(&to.Trailer.FieldMap) + + to.ReceiveTime = m.ReceiveTime + to.bodyBytes = make([]byte, len(m.bodyBytes)) + copy(to.bodyBytes, m.bodyBytes) + to.fields = make([]TagValue, len(m.fields)) + for i := range to.fields { + to.fields[i].init(m.fields[i].tag, m.fields[i].value) + } +} + //ParseMessage constructs a Message from a byte slice wrapping a FIX message. func ParseMessage(msg *Message, rawMessage *bytes.Buffer) (err error) { return ParseMessageWithDataDictionary(msg, rawMessage, nil, nil) @@ -357,6 +373,21 @@ func (m *Message) build() []byte { return b.Bytes() } +// Constructs a []byte from a Message instance, using the given bodyBytes. +// This is a workaround for the fact that we currently rely on the generated Message types to properly serialize/deserialize RepeatingGroups. +// In other words, we cannot go from bytes to a Message then back to bytes, which is exactly what we need to do in the case of a Resend. +// This func lets us pull the Message from the Store, parse it, update the Header, and then build it back into bytes using the original Body. +// Note: The only standard non-Body group is NoHops. If that is used in the Header, this workaround may fail. +func (m *Message) buildWithBodyBytes(bodyBytes []byte) []byte { + m.cook() + + var b bytes.Buffer + m.Header.write(&b) + b.Write(bodyBytes) + m.Trailer.write(&b) + return b.Bytes() +} + func (m *Message) cook() { bodyLength := m.Header.length() + m.Body.length() + m.Trailer.length() m.Header.SetInt(tagBodyLength, bodyLength) diff --git a/message_test.go b/message_test.go index fb7caf9a9..59840d909 100644 --- a/message_test.go +++ b/message_test.go @@ -2,6 +2,7 @@ package quickfix import ( "bytes" + "reflect" "testing" "github.com/quickfixgo/quickfix/datadictionary" @@ -106,18 +107,54 @@ func (s *MessageSuite) TestReBuild() { s.msg.Header.SetField(tagOrigSendingTime, FIXString("20140515-19:49:56.659")) s.msg.Header.SetField(tagSendingTime, FIXString("20140615-19:49:56")) + s.msg.Header.SetField(tagPossDupFlag, FIXBoolean(true)) rebuildBytes := s.msg.build() - expectedBytes := []byte("8=FIX.4.29=12635=D34=249=TW52=20140615-19:49:5656=ISLD122=20140515-19:49:56.65911=10021=140=154=155=TSLA60=00010101-00:00:00.00010=128") + expectedBytes := []byte("8=FIX.4.29=13135=D34=243=Y49=TW52=20140615-19:49:5656=ISLD122=20140515-19:49:56.65911=10021=140=154=155=TSLA60=00010101-00:00:00.00010=122") - s.True(bytes.Equal(expectedBytes, rebuildBytes), "Unexpected bytes,\n +%s\n-%s", rebuildBytes, expectedBytes) + s.True(bytes.Equal(expectedBytes, rebuildBytes), "Unexpected bytes,\n +%s\n -%s", rebuildBytes, expectedBytes) expectedBodyBytes := []byte("11=10021=140=154=155=TSLA60=00010101-00:00:00.000") s.True(bytes.Equal(s.msg.bodyBytes, expectedBodyBytes), "Incorrect body bytes, got %s", string(s.msg.bodyBytes)) } +func (s *MessageSuite) TestReBuildWithRepeatingGroupForResend() { + // Given the following message with a repeating group + origHeader := "8=FIXT.1.19=16135=834=349=ISLD52=20240415-03:43:17.92356=TW" + origBody := "6=1.0011=114=1.0017=131=1.0032=1.0037=138=1.0039=254=155=1150=2151=0.00453=1448=xyzzy447=D452=1" + origTrailer := "10=014" + rawMsg := bytes.NewBufferString(origHeader + origBody + origTrailer) + + // When I reparse the message from the store during a resend request + s.Nil(ParseMessage(s.msg, rawMsg)) + + // And I update the headers for resend + s.msg.Header.SetField(tagOrigSendingTime, FIXString("20240415-03:43:17.923")) + s.msg.Header.SetField(tagSendingTime, FIXString("20240415-14:41:23.456")) + s.msg.Header.SetField(tagPossDupFlag, FIXBoolean(true)) + + // When I rebuild the message + rebuildBytes := s.msg.build() + + // Then the repeating groups will not be in the correct order in the rebuilt message (note tags 447, 448, 452, 453) + expectedBytes := []byte("8=FIXT.1.19=19235=834=343=Y49=ISLD52=20240415-14:41:23.45656=TW122=20240415-03:43:17.9236=1.0011=114=1.0017=131=1.0032=1.0037=138=1.0039=254=155=1150=2151=0.00453=1448=xyzzy447=D452=110=018") + s.False(bytes.Equal(expectedBytes, rebuildBytes), "Unexpected bytes,\n expected: %s\n but was: %s", expectedBytes, rebuildBytes) + expectedOutOfOrderBytes := []byte("8=FIXT.1.19=19235=834=343=Y49=ISLD52=20240415-14:41:23.45656=TW122=20240415-03:43:17.9236=1.0011=114=1.0017=131=1.0032=1.0037=138=1.0039=254=155=1150=2151=0.00447=D448=xyzzy452=1453=110=018") + s.True(bytes.Equal(expectedOutOfOrderBytes, rebuildBytes), "Unexpected bytes,\n expected: %s\n but was: %s", expectedOutOfOrderBytes, rebuildBytes) + + // But the bodyBytes will still be correct + origBodyBytes := []byte(origBody) + s.True(bytes.Equal(origBodyBytes, s.msg.bodyBytes), "Incorrect body bytes, \n expected: %s\n but was: %s", origBodyBytes, s.msg.bodyBytes) + + // So when I combine the updated header + the original bodyBytes + the as-is trailer + resendBytes := s.msg.buildWithBodyBytes(s.msg.bodyBytes) + + // Then the reparsed, rebuilt message will retain the correct ordering of repeating group tags during resend + s.True(bytes.Equal(expectedBytes, resendBytes), "Unexpected bytes,\n expected: %s\n but was: %s", expectedBytes, resendBytes) +} + func (s *MessageSuite) TestReverseRoute() { s.Nil(ParseMessage(s.msg, bytes.NewBufferString("8=FIX.4.29=17135=D34=249=TW50=KK52=20060102-15:04:0556=ISLD57=AP144=BB115=JCD116=CS128=MG129=CB142=JV143=RY145=BH11=ID21=338=10040=w54=155=INTC60=20060102-15:04:0510=123"))) @@ -166,3 +203,50 @@ func (s *MessageSuite) TestReverseRouteFIX40() { s.False(builder.Header.Has(tagOnBehalfOfLocationID), "onbehalfof location id not supported in fix40") } + +func (s *MessageSuite) TestCopyIntoMessage() { + msgString := "8=FIX.4.29=17135=D34=249=TW50=KK52=20060102-15:04:0556=ISLD57=AP144=BB115=JCD116=CS128=MG129=CB142=JV143=RY145=BH11=ID21=338=10040=w54=155=INTC60=20060102-15:04:0510=123" + msgBuf := bytes.NewBufferString(msgString) + s.Nil(ParseMessage(s.msg, msgBuf)) + + dest := NewMessage() + s.msg.CopyInto(dest) + + checkFieldInt(s, dest.Header.FieldMap, int(tagMsgSeqNum), 2) + checkFieldInt(s, dest.Body.FieldMap, 21, 3) + checkFieldString(s, dest.Body.FieldMap, 11, "ID") + s.Equal(len(dest.bodyBytes), len(s.msg.bodyBytes)) + + // copying decouples the message from its input buffer, so the raw message will be re-rendered + renderedString := "8=FIX.4.29=17135=D34=249=TW50=KK52=20060102-15:04:0556=ISLD57=AP115=JCD116=CS128=MG129=CB142=JV143=RY144=BB145=BH11=ID21=338=10040=w54=155=INTC60=20060102-15:04:0510=033" + s.Equal(dest.String(), renderedString) + + s.True(reflect.DeepEqual(s.msg.bodyBytes, dest.bodyBytes)) + s.True(s.msg.IsMsgTypeOf("D")) + s.Equal(s.msg.ReceiveTime, dest.ReceiveTime) + + s.True(reflect.DeepEqual(s.msg.fields, dest.fields)) + + // update the source message to validate the copy is truly deep + newMsgString := "8=FIX.4.49=4935=A52=20140615-19:49:56553=my_user554=secret10=072" + s.Nil(ParseMessage(s.msg, bytes.NewBufferString(newMsgString))) + s.True(s.msg.IsMsgTypeOf("A")) + s.Equal(s.msg.String(), newMsgString) + + // clear the source buffer also + msgBuf.Reset() + + s.True(dest.IsMsgTypeOf("D")) + s.Equal(dest.String(), renderedString) +} + +func checkFieldInt(s *MessageSuite, fields FieldMap, tag, expected int) { + toCheck, _ := fields.GetInt(Tag(tag)) + s.Equal(expected, toCheck) +} + +func checkFieldString(s *MessageSuite, fields FieldMap, tag int, expected string) { + toCheck, err := fields.GetString(Tag(tag)) + s.NoError(err) + s.Equal(expected, toCheck) +} diff --git a/mongostore.go b/mongostore.go new file mode 100644 index 000000000..e50af2c16 --- /dev/null +++ b/mongostore.go @@ -0,0 +1,283 @@ +package quickfix + +import ( + "fmt" + "time" + + "github.com/globalsign/mgo" + "github.com/globalsign/mgo/bson" + "github.com/pkg/errors" + "github.com/quickfixgo/quickfix/config" +) + +type mongoStoreFactory struct { + settings *Settings + messagesCollection string + sessionsCollection string +} + +type mongoStore struct { + sessionID SessionID + cache *memoryStore + mongoURL string + mongoDatabase string + db *mgo.Session + messagesCollection string + sessionsCollection string +} + +// NewMongoStoreFactory returns a mongo-based implementation of MessageStoreFactory +func NewMongoStoreFactory(settings *Settings) MessageStoreFactory { + return NewMongoStoreFactoryPrefixed(settings, "") +} + +// NewMongoStoreFactoryPrefixed returns a mongo-based implementation of MessageStoreFactory, with prefix on collections +func NewMongoStoreFactoryPrefixed(settings *Settings, collectionsPrefix string) MessageStoreFactory { + return mongoStoreFactory{ + settings: settings, + messagesCollection: collectionsPrefix + "messages", + sessionsCollection: collectionsPrefix + "sessions", + } +} + +// Create creates a new MongoStore implementation of the MessageStore interface +func (f mongoStoreFactory) Create(sessionID SessionID) (msgStore MessageStore, err error) { + sessionSettings, ok := f.settings.SessionSettings()[sessionID] + if !ok { + return nil, fmt.Errorf("unknown session: %v", sessionID) + } + mongoConnectionURL, err := sessionSettings.Setting(config.MongoStoreConnection) + if err != nil { + return nil, err + } + mongoDatabase, err := sessionSettings.Setting(config.MongoStoreDatabase) + if err != nil { + return nil, err + } + return newMongoStore(sessionID, mongoConnectionURL, mongoDatabase, f.messagesCollection, f.sessionsCollection) +} + +func newMongoStore(sessionID SessionID, mongoURL string, mongoDatabase string, messagesCollection string, sessionsCollection string) (store *mongoStore, err error) { + store = &mongoStore{ + sessionID: sessionID, + cache: &memoryStore{}, + mongoURL: mongoURL, + mongoDatabase: mongoDatabase, + messagesCollection: messagesCollection, + sessionsCollection: sessionsCollection, + } + + if err = store.cache.Reset(); err != nil { + err = errors.Wrap(err, "cache reset") + return + } + + if store.db, err = mgo.Dial(mongoURL); err != nil { + return + } + err = store.populateCache() + + return +} + +func generateMessageFilter(s *SessionID) (messageFilter *mongoQuickFixEntryData) { + messageFilter = &mongoQuickFixEntryData{ + BeginString: s.BeginString, + SessionQualifier: s.Qualifier, + SenderCompID: s.SenderCompID, + SenderSubID: s.SenderSubID, + SenderLocID: s.SenderLocationID, + TargetCompID: s.TargetCompID, + TargetSubID: s.TargetSubID, + TargetLocID: s.TargetLocationID, + } + return +} + +type mongoQuickFixEntryData struct { + //Message specific data + Msgseq int `bson:"msgseq,omitempty"` + Message []byte `bson:"message,omitempty"` + //Session specific data + CreationTime time.Time `bson:"creation_time,omitempty"` + IncomingSeqNum int `bson:"incoming_seq_num,omitempty"` + OutgoingSeqNum int `bson:"outgoing_seq_num,omitempty"` + //Indexed data + BeginString string `bson:"begin_string"` + SessionQualifier string `bson:"session_qualifier"` + SenderCompID string `bson:"sender_comp_id"` + SenderSubID string `bson:"sender_sub_id"` + SenderLocID string `bson:"sender_loc_id"` + TargetCompID string `bson:"target_comp_id"` + TargetSubID string `bson:"target_sub_id"` + TargetLocID string `bson:"target_loc_id"` +} + +// Reset deletes the store records and sets the seqnums back to 1 +func (store *mongoStore) Reset() error { + msgFilter := generateMessageFilter(&store.sessionID) + _, err := store.db.DB(store.mongoDatabase).C(store.messagesCollection).RemoveAll(msgFilter) + + if err != nil { + return err + } + + if err = store.cache.Reset(); err != nil { + return err + } + + sessionUpdate := generateMessageFilter(&store.sessionID) + sessionUpdate.CreationTime = store.cache.CreationTime() + sessionUpdate.IncomingSeqNum = store.cache.NextTargetMsgSeqNum() + sessionUpdate.OutgoingSeqNum = store.cache.NextSenderMsgSeqNum() + err = store.db.DB(store.mongoDatabase).C(store.sessionsCollection).Update(msgFilter, sessionUpdate) + + return err +} + +// Refresh reloads the store from the database +func (store *mongoStore) Refresh() error { + if err := store.cache.Reset(); err != nil { + return err + } + return store.populateCache() +} + +func (store *mongoStore) populateCache() error { + msgFilter := generateMessageFilter(&store.sessionID) + query := store.db.DB(store.mongoDatabase).C(store.sessionsCollection).Find(msgFilter) + + cnt, err := query.Count() + if err != nil { + return errors.Wrap(err, "count") + } + + if cnt > 0 { + // session record found, load it + sessionData := &mongoQuickFixEntryData{} + if err = query.One(&sessionData); err != nil { + return errors.Wrap(err, "query one") + } + + store.cache.creationTime = sessionData.CreationTime + if err = store.cache.SetNextTargetMsgSeqNum(sessionData.IncomingSeqNum); err != nil { + return errors.Wrap(err, "cache set next target") + } + + if err = store.cache.SetNextSenderMsgSeqNum(sessionData.OutgoingSeqNum); err != nil { + return errors.Wrap(err, "cache set next sender") + } + + return nil + } + + // session record not found, create it + msgFilter.CreationTime = store.cache.creationTime + msgFilter.IncomingSeqNum = store.cache.NextTargetMsgSeqNum() + msgFilter.OutgoingSeqNum = store.cache.NextSenderMsgSeqNum() + + if err = store.db.DB(store.mongoDatabase).C(store.sessionsCollection).Insert(msgFilter); err != nil { + return errors.Wrap(err, "insert") + } + return nil +} + +// NextSenderMsgSeqNum returns the next MsgSeqNum that will be sent +func (store *mongoStore) NextSenderMsgSeqNum() int { + return store.cache.NextSenderMsgSeqNum() +} + +// NextTargetMsgSeqNum returns the next MsgSeqNum that should be received +func (store *mongoStore) NextTargetMsgSeqNum() int { + return store.cache.NextTargetMsgSeqNum() +} + +// SetNextSenderMsgSeqNum sets the next MsgSeqNum that will be sent +func (store *mongoStore) SetNextSenderMsgSeqNum(next int) error { + msgFilter := generateMessageFilter(&store.sessionID) + sessionUpdate := generateMessageFilter(&store.sessionID) + sessionUpdate.IncomingSeqNum = store.cache.NextTargetMsgSeqNum() + sessionUpdate.OutgoingSeqNum = next + sessionUpdate.CreationTime = store.cache.CreationTime() + if err := store.db.DB(store.mongoDatabase).C(store.sessionsCollection).Update(msgFilter, sessionUpdate); err != nil { + return err + } + return store.cache.SetNextSenderMsgSeqNum(next) +} + +// SetNextTargetMsgSeqNum sets the next MsgSeqNum that should be received +func (store *mongoStore) SetNextTargetMsgSeqNum(next int) error { + msgFilter := generateMessageFilter(&store.sessionID) + sessionUpdate := generateMessageFilter(&store.sessionID) + sessionUpdate.IncomingSeqNum = next + sessionUpdate.OutgoingSeqNum = store.cache.NextSenderMsgSeqNum() + sessionUpdate.CreationTime = store.cache.CreationTime() + if err := store.db.DB(store.mongoDatabase).C(store.sessionsCollection).Update(msgFilter, sessionUpdate); err != nil { + return err + } + return store.cache.SetNextTargetMsgSeqNum(next) +} + +// IncrNextSenderMsgSeqNum increments the next MsgSeqNum that will be sent +func (store *mongoStore) IncrNextSenderMsgSeqNum() error { + if err := store.cache.IncrNextSenderMsgSeqNum(); err != nil { + return errors.Wrap(err, "cache incr") + } + return store.SetNextSenderMsgSeqNum(store.cache.NextSenderMsgSeqNum()) +} + +// IncrNextTargetMsgSeqNum increments the next MsgSeqNum that should be received +func (store *mongoStore) IncrNextTargetMsgSeqNum() error { + if err := store.cache.IncrNextTargetMsgSeqNum(); err != nil { + return errors.Wrap(err, "cache incr") + } + return store.SetNextTargetMsgSeqNum(store.cache.NextTargetMsgSeqNum()) +} + +// CreationTime returns the creation time of the store +func (store *mongoStore) CreationTime() time.Time { + return store.cache.CreationTime() +} + +func (store *mongoStore) SaveMessage(seqNum int, msg []byte) (err error) { + msgFilter := generateMessageFilter(&store.sessionID) + msgFilter.Msgseq = seqNum + msgFilter.Message = msg + err = store.db.DB(store.mongoDatabase).C(store.messagesCollection).Insert(msgFilter) + return +} + +func (store *mongoStore) GetMessages(beginSeqNum, endSeqNum int) (msgs [][]byte, err error) { + msgFilter := generateMessageFilter(&store.sessionID) + //Marshal into database form + msgFilterBytes, err := bson.Marshal(msgFilter) + if err != nil { + return + } + seqFilter := bson.M{} + err = bson.Unmarshal(msgFilterBytes, &seqFilter) + if err != nil { + return + } + //Modify the query to use a range for the sequence filter + seqFilter["msgseq"] = bson.M{ + "$gte": beginSeqNum, + "$lte": endSeqNum, + } + + iter := store.db.DB(store.mongoDatabase).C(store.messagesCollection).Find(seqFilter).Sort("msgseq").Iter() + for iter.Next(msgFilter) { + msgs = append(msgs, msgFilter.Message) + } + err = iter.Close() + return +} + +// Close closes the store's database connection +func (store *mongoStore) Close() error { + if store.db != nil { + store.db.Close() + store.db = nil + } + return nil +} diff --git a/mongostore_test.go b/mongostore_test.go new file mode 100644 index 000000000..29d43d71d --- /dev/null +++ b/mongostore_test.go @@ -0,0 +1,53 @@ +package quickfix + +import ( + "fmt" + "log" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +// MongoStoreTestSuite runs all tests in the MessageStoreTestSuite against the MongoStore implementation +type MongoStoreTestSuite struct { + MessageStoreTestSuite +} + +func (suite *MongoStoreTestSuite) SetupTest() { + mongoDbCxn := os.Getenv("MONGODB_TEST_CXN") + if len(mongoDbCxn) <= 0 { + log.Println("MONGODB_TEST_CXN environment arg is not provided, skipping...") + suite.T().SkipNow() + } + mongoDatabase := "automated_testing_database" + + // create settings + sessionID := SessionID{BeginString: "FIX.4.4", SenderCompID: "SENDER", TargetCompID: "TARGET"} + settings, err := ParseSettings(strings.NewReader(fmt.Sprintf(` +[DEFAULT] +MongoStoreConnection=%s +MongoStoreDatabase=%s + +[SESSION] +BeginString=%s +SenderCompID=%s +TargetCompID=%s`, mongoDbCxn, mongoDatabase, sessionID.BeginString, sessionID.SenderCompID, sessionID.TargetCompID))) + require.Nil(suite.T(), err) + + // create store + suite.msgStore, err = NewMongoStoreFactory(settings).Create(sessionID) + require.Nil(suite.T(), err) + err = suite.msgStore.Reset() + require.Nil(suite.T(), err) +} + +func (suite *MongoStoreTestSuite) TearDownTest() { + suite.msgStore.Close() +} + +func TestMongoStoreTestSuite(t *testing.T) { + suite.Run(t, new(MongoStoreTestSuite)) +} diff --git a/not_session_time.go b/not_session_time.go index 2e3b04c3d..5648a6eb2 100644 --- a/not_session_time.go +++ b/not_session_time.go @@ -4,7 +4,7 @@ import "github.com/quickfixgo/quickfix/internal" type notSessionTime struct{ latentState } -func (notSessionTime) String() string { return "Not session time" } +func (notSessionTime) String() string { return SessionStateNotSessionTime } func (notSessionTime) IsSessionTime() bool { return false } func (state notSessionTime) FixMsgIn(session *session, msg *Message) (nextState sessionState) { diff --git a/nuts_store.go b/nuts_store.go new file mode 100644 index 000000000..597eaa0cf --- /dev/null +++ b/nuts_store.go @@ -0,0 +1,184 @@ +package quickfix + +import ( + "encoding/binary" + "time" + + "github.com/nutsdb/nutsdb" + "github.com/pkg/errors" +) + +var ( + keyMessages = []byte("messages") + keyOutgoingSeqnum = []byte("outgoing_seqnum") + keyIncomingSeqnum = []byte("incoming_seqnum") +) + +type nutsDbStoreFactory struct { + db *nutsdb.DB +} + +func NewNutsDbStoreFactory(db *nutsdb.DB) MessageStoreFactory { + return nutsDbStoreFactory{db: db} +} + +func (f nutsDbStoreFactory) Create(sessionID SessionID) (msgStore MessageStore, err error) { + sessionPrefix := SessionIDFilenamePrefix(sessionID) + store := &nutsDbStore{ + db: f.db, + cache: &memoryStore{}, + bucket: sessionPrefix, + } + if err = store.cache.Reset(); err != nil { + err = errors.Wrap(err, "cache reset") + return + } + if err = store.populateCache(); err != nil { + return nil, err + } + return store, nil +} + +type nutsDbStore struct { + cache *memoryStore + + bucket string + db *nutsdb.DB +} + +func (store *nutsDbStore) NextSenderMsgSeqNum() int { + return store.cache.NextSenderMsgSeqNum() +} + +func (store *nutsDbStore) NextTargetMsgSeqNum() int { + return store.cache.NextTargetMsgSeqNum() +} + +func (store *nutsDbStore) IncrNextSenderMsgSeqNum() error { + if err := store.cache.IncrNextSenderMsgSeqNum(); err != nil { + return errors.Wrap(err, "cache incr next") + } + return store.SetNextSenderMsgSeqNum(store.cache.NextSenderMsgSeqNum()) +} + +func (store *nutsDbStore) IncrNextTargetMsgSeqNum() error { + if err := store.cache.IncrNextTargetMsgSeqNum(); err != nil { + return errors.Wrap(err, "cache incr next") + } + return store.SetNextTargetMsgSeqNum(store.cache.NextTargetMsgSeqNum()) +} + +func (store *nutsDbStore) SetNextSenderMsgSeqNum(next int) error { + + err := store.db.Update(func(tx *nutsdb.Tx) error { + nextBuf := make([]byte, 8) + binary.BigEndian.PutUint64(nextBuf, uint64(next)) + return tx.Put(store.bucket, keyOutgoingSeqnum, nextBuf, nutsdb.Persistent) + }) + + if err != nil { + return err + } + return store.cache.SetNextSenderMsgSeqNum(next) +} +func (store *nutsDbStore) SetNextTargetMsgSeqNum(next int) error { + + err := store.db.Update(func(tx *nutsdb.Tx) error { + nextBuf := make([]byte, 8) + binary.BigEndian.PutUint64(nextBuf, uint64(next)) + return tx.Put(store.bucket, keyIncomingSeqnum, nextBuf, nutsdb.Persistent) + }) + if err != nil { + return err + } + return store.cache.SetNextTargetMsgSeqNum(next) +} + +func (store *nutsDbStore) CreationTime() time.Time { + return store.cache.CreationTime() +} + +func (store *nutsDbStore) Reset() error { + return store.db.Update(func(tx *nutsdb.Tx) error { + // err := tx.Delete(store.bucket, keyMessages) + // if err != nil { + // return err + // } + err := tx.DeleteBucket(nutsdb.DataStructureList, store.bucket) + if err = store.cache.Reset(); err != nil { + return err + } + nextBuf := make([]byte, 8) + binary.BigEndian.PutUint64(nextBuf, uint64(store.cache.NextSenderMsgSeqNum())) + err = tx.Put(store.bucket, keyOutgoingSeqnum, nextBuf, nutsdb.Persistent) + if err != nil { + return err + } + binary.BigEndian.PutUint64(nextBuf, uint64(store.cache.NextTargetMsgSeqNum())) + return tx.Put(store.bucket, keyIncomingSeqnum, nextBuf, nutsdb.Persistent) + }) +} + +func (store *nutsDbStore) Refresh() error { + if err := store.cache.Reset(); err != nil { + return err + } + return store.populateCache() + +} +func (store *nutsDbStore) populateCache() error { + return store.db.Update(func(tx *nutsdb.Tx) error { + var incomingSeqNum, outgoingSeqNum int + + if e, err := tx.Get(store.bucket, keyOutgoingSeqnum); err == nil { + outgoingSeqNum = int(binary.BigEndian.Uint64(e.Value)) + if err := store.cache.SetNextSenderMsgSeqNum(outgoingSeqNum); err != nil { + return errors.Wrap(err, "cache set next sender") + } + } else { + nextBuf := make([]byte, 8) + binary.BigEndian.PutUint64(nextBuf, uint64(store.cache.NextSenderMsgSeqNum())) + if err := tx.Put(store.bucket, keyOutgoingSeqnum, nextBuf, nutsdb.Persistent); err != nil { + return err + } + + } + + if e, err := tx.Get(store.bucket, keyIncomingSeqnum); err == nil { + incomingSeqNum = int(binary.BigEndian.Uint64(e.Value)) + if err := store.cache.SetNextTargetMsgSeqNum(incomingSeqNum); err != nil { + return errors.Wrap(err, "cache set next target") + } + } else { + nextBuf := make([]byte, 8) + binary.BigEndian.PutUint64(nextBuf, uint64(store.cache.NextTargetMsgSeqNum())) + if err := tx.Put(store.bucket, keyIncomingSeqnum, nextBuf, nutsdb.Persistent); err != nil { + return err + } + } + + return nil + }) +} + +func (store *nutsDbStore) SaveMessage(seqNum int, msg []byte) error { + return store.db.Update(func(tx *nutsdb.Tx) error { + return tx.RPush(store.bucket, keyMessages, msg) + }) +} + +func (store *nutsDbStore) GetMessages(beginSeqNum, endSeqNum int) ([][]byte, error) { + var msgs [][]byte + store.db.View(func(tx *nutsdb.Tx) error { + msgs, _ = tx.LRange(store.bucket, keyMessages, beginSeqNum, endSeqNum) + return nil + }) + return msgs, nil +} + +func (store *nutsDbStore) Close() error { + if store.db != nil { + return store.db.Close() + } + return nil +} diff --git a/nuts_store_test.go b/nuts_store_test.go new file mode 100644 index 000000000..0a59292fb --- /dev/null +++ b/nuts_store_test.go @@ -0,0 +1,81 @@ +package quickfix + +import ( + "fmt" + "os" + "path" + "testing" + + "github.com/nutsdb/nutsdb" + "github.com/stretchr/testify/require" +) + +func TestSetup(t *testing.T) { + // create settings + sessionID := SessionID{BeginString: "FIX.4.4", SenderCompID: "SENDER", TargetCompID: "TARGET"} + // settings, err := ParseSettings(strings.NewReader(fmt.Sprintf(` + // [DEFAULT] + // TimeStampPrecision=MICROS + + // [SESSION] + // Tenant=lb_hk + // BeginString=%s + // SenderCompID=%s + // TargetCompID=%s`, sessionID.BeginString, sessionID.SenderCompID, sessionID.TargetCompID))) + // require.Nil(t, err) + + file := path.Join(os.TempDir(), fmt.Sprintf("%d", os.Getpid())) + file = path.Join(file, "/tmp/nutsdb") + + db, err := nutsdb.Open( + nutsdb.DefaultOptions, + nutsdb.WithSyncEnable(false), + nutsdb.WithRWMode(nutsdb.MMap), + nutsdb.WithDir(file), + nutsdb.WithEntryIdxMode(nutsdb.HintKeyValAndRAMIdxMode), + ) + require.Nil(t, err) + + defer os.RemoveAll(file) + + messageStoreFactory := NewNutsDbStoreFactory(db) + store, err := messageStoreFactory.Create(sessionID) + require.Nil(t, err) + require.NotNil(t, store) + + for i := 0; i < 10; i++ { + err = store.SaveMessage(i, []byte(fmt.Sprintf("hello_%v", i))) + require.Nil(t, err) + err = store.IncrNextSenderMsgSeqNum() + require.Nil(t, err) + } + + bs, err := store.GetMessages(0, 1) + require.Nil(t, err) + require.Equal(t, len(bs), 2) + + bs, err = store.GetMessages(1, 10) + require.Nil(t, err) + require.Equal(t, len(bs), 9) + + err = store.Reset() + require.Nil(t, err) + + bs, err = store.GetMessages(0, 1) + require.Nil(t, err) + require.Equal(t, len(bs), 0) + + for i := 0; i < 100; i++ { + err = store.SaveMessage(i, []byte(fmt.Sprintf("hello_%v", i))) + require.Nil(t, err) + err = store.IncrNextSenderMsgSeqNum() + require.Nil(t, err) + } + + bs, err = store.GetMessages(0, 100) + require.Nil(t, err) + require.Equal(t, len(bs), 100) + + err = store.Close() + require.Nil(t, err) +} diff --git a/parser_test.go b/parser_test.go index ab9c086a7..533691986 100644 --- a/parser_test.go +++ b/parser_test.go @@ -13,7 +13,7 @@ func BenchmarkParser_ReadMessage(b *testing.B) { for i := 0; i < b.N; i++ { reader := strings.NewReader(stream) parser := newParser(reader) - parser.ReadMessage() + _, _ = parser.ReadMessage() } } diff --git a/quickfix_test.go b/quickfix_test.go index 2f511b77d..e24688eeb 100644 --- a/quickfix_test.go +++ b/quickfix_test.go @@ -3,10 +3,11 @@ package quickfix import ( "time" - "github.com/quickfixgo/quickfix/internal" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + + "github.com/quickfixgo/quickfix/internal" ) type QuickFIXSuite struct { @@ -72,6 +73,10 @@ type MockApp struct { func (e *MockApp) OnCreate(sessionID SessionID) { } +func (e *MockApp) OnEvent(sessionID SessionID, tp EventType, ev interface{}) { + +} + func (e *MockApp) OnLogon(sessionID SessionID) { e.Called() } diff --git a/registry.go b/registry.go index ac074d2da..728a271a2 100644 --- a/registry.go +++ b/registry.go @@ -30,12 +30,21 @@ func Send(m Messagable) (err error) { var senderCompID FIXString if err := msg.Header.GetField(tagSenderCompID, &senderCompID); err != nil { - - return nil + return err } - sessionID := SessionID{BeginString: string(beginString), TargetCompID: string(targetCompID), SenderCompID: string(senderCompID)} + var senderSubID FIXString + msg.Header.GetField(tagSenderSubID, &senderSubID) + + var targetSubID FIXString + msg.Header.GetField(tagTargetSubID, &targetSubID) + if len(senderSubID) > 0 && len(targetSubID) > 0 { + sessionID := SessionID{BeginString: beginString.String(), TargetCompID: targetCompID.String(), SenderCompID: senderCompID.String(), SenderSubID: senderSubID.String(), TargetSubID: targetSubID.String()} + return SendToTarget(msg, sessionID) + } + + sessionID := SessionID{BeginString: beginString.String(), TargetCompID: targetCompID.String(), SenderCompID: senderCompID.String()} return SendToTarget(msg, sessionID) } @@ -50,6 +59,19 @@ func SendToTarget(m Messagable, sessionID SessionID) error { return session.queueForSend(msg) } +//UnregisterSession removes a session from the set of known sessions +func UnregisterSession(sessionID SessionID) error { + sessionsLock.Lock() + defer sessionsLock.Unlock() + + if _, ok := sessions[sessionID]; ok { + delete(sessions, sessionID) + return nil + } + + return errUnknownSession +} + func registerSession(s *session) error { sessionsLock.Lock() defer sessionsLock.Unlock() diff --git a/repeating_group.go b/repeating_group.go index d3bc4a58c..811379c37 100644 --- a/repeating_group.go +++ b/repeating_group.go @@ -109,7 +109,7 @@ func (f *RepeatingGroup) Add() *Group { //Write returns tagValues for all Items in the repeating group ordered by //Group sequence and Group template order func (f RepeatingGroup) Write() []TagValue { - tvs := make([]TagValue, 1, 1) + tvs := make([]TagValue, 1) tvs[0].init(f.tag, []byte(strconv.Itoa(len(f.groups)))) for _, group := range f.groups { diff --git a/repeating_group_test.go b/repeating_group_test.go index d43d38d79..448e39d6a 100644 --- a/repeating_group_test.go +++ b/repeating_group_test.go @@ -97,16 +97,16 @@ func TestRepeatingGroup_ReadError(t *testing.T) { }{ { []TagValue{ - TagValue{value: []byte("1")}, - TagValue{tag: Tag(2), value: []byte("not in template")}, - TagValue{tag: Tag(1), value: []byte("hello")}, + {value: []byte("1")}, + {tag: Tag(2), value: []byte("not in template")}, + {tag: Tag(1), value: []byte("hello")}, }, 0}, { []TagValue{ - TagValue{value: []byte("2")}, - TagValue{tag: Tag(1), value: []byte("hello")}, - TagValue{tag: Tag(2), value: []byte("not in template")}, - TagValue{tag: Tag(1), value: []byte("hello")}, + {value: []byte("2")}, + {tag: Tag(1), value: []byte("hello")}, + {tag: Tag(2), value: []byte("not in template")}, + {tag: Tag(1), value: []byte("hello")}, }, 1}} for _, s := range tests { @@ -128,29 +128,29 @@ func TestRepeatingGroup_Read(t *testing.T) { tv []TagValue expectedGroupTvs [][]TagValue }{ - {singleFieldTemplate, []TagValue{TagValue{value: []byte("0")}}, + {singleFieldTemplate, []TagValue{{value: []byte("0")}}, [][]TagValue{}}, - {singleFieldTemplate, []TagValue{TagValue{value: []byte("1")}, TagValue{tag: Tag(1), value: []byte("hello")}}, + {singleFieldTemplate, []TagValue{{value: []byte("1")}, {tag: Tag(1), value: []byte("hello")}}, [][]TagValue{{TagValue{tag: Tag(1), value: []byte("hello")}}}}, {singleFieldTemplate, - []TagValue{TagValue{value: []byte("1")}, - TagValue{tag: Tag(1), value: []byte("hello")}, - TagValue{tag: Tag(2), value: []byte("not in group")}}, + []TagValue{{value: []byte("1")}, + {tag: Tag(1), value: []byte("hello")}, + {tag: Tag(2), value: []byte("not in group")}}, [][]TagValue{ {TagValue{tag: Tag(1), value: []byte("hello")}}}}, {singleFieldTemplate, - []TagValue{TagValue{value: []byte("2")}, - TagValue{tag: Tag(1), value: []byte("hello")}, - TagValue{tag: Tag(1), value: []byte("world")}}, + []TagValue{{value: []byte("2")}, + {tag: Tag(1), value: []byte("hello")}, + {tag: Tag(1), value: []byte("world")}}, [][]TagValue{ {TagValue{tag: Tag(1), value: []byte("hello")}}, {TagValue{tag: Tag(1), value: []byte("world")}}, }}, {multiFieldTemplate, []TagValue{ - TagValue{value: []byte("2")}, - TagValue{tag: Tag(1), value: []byte("hello")}, - TagValue{tag: Tag(1), value: []byte("goodbye")}, TagValue{tag: Tag(2), value: []byte("cruel")}, TagValue{tag: Tag(3), value: []byte("world")}, + {value: []byte("2")}, + {tag: Tag(1), value: []byte("hello")}, + {tag: Tag(1), value: []byte("goodbye")}, {tag: Tag(2), value: []byte("cruel")}, {tag: Tag(3), value: []byte("world")}, }, [][]TagValue{ {TagValue{tag: Tag(1), value: []byte("hello")}}, @@ -158,10 +158,10 @@ func TestRepeatingGroup_Read(t *testing.T) { }}, {multiFieldTemplate, []TagValue{ - TagValue{value: []byte("3")}, - TagValue{tag: Tag(1), value: []byte("hello")}, - TagValue{tag: Tag(1), value: []byte("goodbye")}, TagValue{tag: Tag(2), value: []byte("cruel")}, TagValue{tag: Tag(3), value: []byte("world")}, - TagValue{tag: Tag(1), value: []byte("another")}, + {value: []byte("3")}, + {tag: Tag(1), value: []byte("hello")}, + {tag: Tag(1), value: []byte("goodbye")}, {tag: Tag(2), value: []byte("cruel")}, {tag: Tag(3), value: []byte("world")}, + {tag: Tag(1), value: []byte("another")}, }, [][]TagValue{ {TagValue{tag: Tag(1), value: []byte("hello")}}, @@ -200,11 +200,11 @@ func TestRepeatingGroup_ReadRecursive(t *testing.T) { f := NewRepeatingGroup(Tag(1), parentTemplate) _, err := f.Read([]TagValue{ - TagValue{value: []byte("2")}, - TagValue{tag: Tag(2), value: []byte("hello")}, - TagValue{tag: 3, value: []byte("1")}, TagValue{tag: 4, value: []byte("foo")}, - TagValue{tag: Tag(2), value: []byte("world")}, - TagValue{tag: 3, value: []byte("2")}, TagValue{tag: 4, value: []byte("foo")}, TagValue{tag: 4, value: []byte("bar")}, TagValue{tag: 5, value: []byte("fubar")}, + {value: []byte("2")}, + {tag: Tag(2), value: []byte("hello")}, + {tag: 3, value: []byte("1")}, {tag: 4, value: []byte("foo")}, + {tag: Tag(2), value: []byte("world")}, + {tag: 3, value: []byte("2")}, {tag: 4, value: []byte("foo")}, {tag: 4, value: []byte("bar")}, {tag: 5, value: []byte("fubar")}, }) require.Nil(t, err) diff --git a/resend_state.go b/resend_state.go index a89d3f697..c0b64df3f 100644 --- a/resend_state.go +++ b/resend_state.go @@ -9,7 +9,7 @@ type resendState struct { resendRangeEnd int } -func (s resendState) String() string { return "Resend" } +func (s resendState) String() string { return SessionStateResend } func (s resendState) Timeout(session *session, event internal.Event) (nextState sessionState) { nextState = inSession{}.Timeout(session, event) diff --git a/session.go b/session.go index e6dde986b..39d97fb93 100644 --- a/session.go +++ b/session.go @@ -11,7 +11,7 @@ import ( "github.com/quickfixgo/quickfix/internal" ) -//The Session is the primary FIX abstraction for message communication +// The Session is the primary FIX abstraction for message communication type session struct { store MessageStore @@ -45,14 +45,17 @@ type session struct { messagePool timestampPrecision TimestampPrecision + + lastConnectData *EventLogon + lastLogonData *EventLogon } func (s *session) logError(err error) { s.log.OnEvent(err.Error()) } -//TargetDefaultApplicationVersionID returns the default application version ID for messages received by this version. -//Applicable for For FIX.T.1 sessions. +// TargetDefaultApplicationVersionID returns the default application version ID for messages received by this version. +// Applicable for For FIX.T.1 sessions. func (s *session) TargetDefaultApplicationVersionID() string { return s.targetDefaultApplVerID } @@ -133,11 +136,20 @@ func (s *session) fillDefaultHeader(msg *Message, inReplyTo *Message) { } } -func (s *session) sendLogon(resetStore, setResetSeqNum bool) error { - return s.sendLogonInReplyTo(resetStore, setResetSeqNum, nil) +func (s *session) shouldSendReset() bool { + if s.sessionID.BeginString < BeginStringFIX41 { + return false + } + + return (s.ResetOnLogon || s.ResetOnDisconnect || s.ResetOnLogout) && + s.store.NextTargetMsgSeqNum() == 1 && s.store.NextSenderMsgSeqNum() == 1 +} + +func (s *session) sendLogon() error { + return s.sendLogonInReplyTo(s.shouldSendReset(), nil) } -func (s *session) sendLogonInReplyTo(resetStore, setResetSeqNum bool, inReplyTo *Message) error { +func (s *session) sendLogonInReplyTo(setResetSeqNum bool, inReplyTo *Message) error { logon := NewMessage() logon.Header.SetField(tagMsgType, FIXString("A")) logon.Header.SetField(tagBeginString, FIXString(s.sessionID.BeginString)) @@ -154,7 +166,7 @@ func (s *session) sendLogonInReplyTo(resetStore, setResetSeqNum bool, inReplyTo logon.Body.SetField(tagDefaultApplVerID, FIXString(s.DefaultApplVerID)) } - if err := s.dropAndSendInReplyTo(logon, resetStore, inReplyTo); err != nil { + if err := s.dropAndSendInReplyTo(logon, inReplyTo); err != nil { return err } @@ -196,7 +208,7 @@ func (s *session) resend(msg *Message) bool { return s.application.ToApp(msg, s.sessionID) == nil } -//queueForSend will validate, persist, and queue the message for send +// queueForSend will validate, persist, and queue the message for send func (s *session) queueForSend(msg *Message) error { s.sendMutex.Lock() defer s.sendMutex.Unlock() @@ -216,7 +228,7 @@ func (s *session) queueForSend(msg *Message) error { return nil } -//send will validate, persist, queue the message. If the session is logged on, send all messages in the queue +// send will validate, persist, queue the message. If the session is logged on, send all messages in the queue func (s *session) send(msg *Message) error { return s.sendInReplyTo(msg, nil) } @@ -239,7 +251,7 @@ func (s *session) sendInReplyTo(msg *Message, inReplyTo *Message) error { return nil } -//dropAndReset will drop the send queue and reset the message store +// dropAndReset will drop the send queue and reset the message store func (s *session) dropAndReset() error { s.sendMutex.Lock() defer s.sendMutex.Unlock() @@ -248,20 +260,14 @@ func (s *session) dropAndReset() error { return s.store.Reset() } -//dropAndSend will optionally reset the store, validate and persist the message, then drops the send queue and sends the message. -func (s *session) dropAndSend(msg *Message, resetStore bool) error { - return s.dropAndSendInReplyTo(msg, resetStore, nil) +// dropAndSend will validate and persist the message, then drops the send queue and sends the message. +func (s *session) dropAndSend(msg *Message) error { + return s.dropAndSendInReplyTo(msg, nil) } -func (s *session) dropAndSendInReplyTo(msg *Message, resetStore bool, inReplyTo *Message) error { +func (s *session) dropAndSendInReplyTo(msg *Message, inReplyTo *Message) error { s.sendMutex.Lock() defer s.sendMutex.Unlock() - if resetStore { - if err := s.store.Reset(); err != nil { - return err - } - } - msgBytes, err := s.prepMessageForSend(msg, inReplyTo) if err != nil { return err @@ -304,7 +310,6 @@ func (s *session) prepMessageForSend(msg *Message, inReplyTo *Message) (msgBytes seqNum = s.store.NextSenderMsgSeqNum() msg.Header.SetField(tagMsgSeqNum, FIXInt(seqNum)) } - } } else { if err = s.application.ToApp(msg, s.sessionID); err != nil { @@ -330,6 +335,9 @@ func (s *session) persist(seqNum int, msgBytes []byte) error { func (s *session) sendQueued() { for _, msgBytes := range s.toSend { + if s.LimitBucket != nil { + s.LimitBucket.Wait() + } s.sendBytes(msgBytes) } @@ -340,6 +348,14 @@ func (s *session) dropQueued() { s.toSend = s.toSend[:0] } +func (s *session) EnqueueBytesAndSend(msg []byte) { + s.sendMutex.Lock() + defer s.sendMutex.Unlock() + + s.toSend = append(s.toSend, msg) + s.sendQueued() +} + func (s *session) sendBytes(msg []byte) { s.log.OnOutgoing(msg) s.messageOut <- msg @@ -413,8 +429,8 @@ func (s *session) handleLogon(msg *Message) error { var resetSeqNumFlag FIXBoolean if err := msg.Body.GetField(tagResetSeqNumFlag, &resetSeqNumFlag); err == nil { if resetSeqNumFlag { - s.log.OnEvent("Logon contains ResetSeqNumFlag=Y, resetting sequence numbers to 1") if !s.sentReset { + s.log.OnEvent("Logon contains ResetSeqNumFlag=Y, resetting sequence numbers to 1") resetStore = true } } @@ -437,7 +453,7 @@ func (s *session) handleLogon(msg *Message) error { } s.log.OnEvent("Responding to logon request") - if err := s.sendLogonInReplyTo(resetStore, resetSeqNumFlag.Bool(), msg); err != nil { + if err := s.sendLogonInReplyTo(resetSeqNumFlag.Bool(), msg); err != nil { return err } } @@ -446,6 +462,11 @@ func (s *session) handleLogon(msg *Message) error { s.peerTimer.Reset(time.Duration(float64(1.2) * float64(s.HeartBtInt))) s.application.OnLogon(s.sessionID) + if s.lastConnectData != nil { + s.application.OnEvent(s.sessionID, EventTypeLogon, s.lastConnectData) + s.lastLogonData = s.lastConnectData + } + if err := s.checkTargetTooHigh(msg); err != nil { return err } @@ -463,8 +484,7 @@ func (s *session) initiateLogoutInReplyTo(reason string, inReplyTo *Message) (er return } s.log.OnEvent("Inititated logout request") - time.AfterFunc(time.Duration(2)*time.Second, func() { s.sessionEvent <- internal.LogoutTimeout }) - + time.AfterFunc(s.LogoutTimeout, func() { s.sessionEvent <- internal.LogoutTimeout }) return } @@ -621,6 +641,9 @@ func (s *session) doReject(msg *Message, rej MessageRejectError) error { if rej.IsBusinessReject() { reply.Header.SetField(tagMsgType, FIXString("j")) reply.Body.SetField(tagBusinessRejectReason, FIXInt(rej.RejectReason())) + if refID := rej.BusinessRejectRefID(); refID != "" { + reply.Body.SetField(tagBusinessRejectRefID, FIXString(refID)) + } } else { reply.Header.SetField(tagMsgType, FIXString("3")) switch { @@ -701,6 +724,15 @@ func (s *session) onAdmin(msg interface{}) { return } + if !s.IsSessionTime() { + s.handleDisconnectState(s) + if msg.err != nil { + msg.err <- errors.New("Connection outside of session time") + close(msg.err) + } + return + } + if msg.err != nil { close(msg.err) } diff --git a/session_factory.go b/session_factory.go index 90e118290..a4b89e48a 100644 --- a/session_factory.go +++ b/session_factory.go @@ -45,7 +45,7 @@ type sessionFactory struct { BuildInitiators bool } -//Creates Session, associates with internal session registry +// Creates Session, associates with internal session registry func (f sessionFactory) createSession( sessionID SessionID, storeFactory MessageStoreFactory, settings *SessionSettings, logFactory LogFactory, application Application, @@ -76,6 +76,12 @@ func (f sessionFactory) newSession( } } + if settings.HasSetting(config.RejectInvalidMessage) { + if validatorSettings.RejectInvalidMessage, err = settings.BoolSetting(config.RejectInvalidMessage); err != nil { + return + } + } + if sessionID.IsFIXT() { if s.DefaultApplVerID, err = settings.Setting(config.DefaultApplVerID); err != nil { return @@ -289,11 +295,20 @@ func (f sessionFactory) newSession( if s.store, err = storeFactory.Create(s.sessionID); err != nil { return } + var sendRatePerSecond int + if settings.HasSetting(config.SendRatePerSecond) { + if sendRatePerSecond, err = settings.IntSetting(config.SendRatePerSecond); err != nil { + return + } + s.LimitBucket = internal.New(time.Second, uint64(sendRatePerSecond)) + } s.sessionEvent = make(chan internal.Event) s.messageEvent = make(chan bool, 1) s.admin = make(chan interface{}) s.application = application + s.stateMachine = stateMachine{State: latentState{}} + return } @@ -325,6 +340,36 @@ func (f sessionFactory) buildInitiatorSettings(session *session, settings *Sessi session.ReconnectInterval = time.Duration(interval) * time.Second } + session.LogoutTimeout = 2 * time.Second + if settings.HasSetting(config.LogoutTimeout) { + + timeout, err := settings.IntSetting(config.LogoutTimeout) + if err != nil { + return err + } + + if timeout <= 0 { + return errors.New("LogoutTimeout must be greater than zero") + } + + session.LogoutTimeout = time.Duration(timeout) * time.Second + } + + session.LogonTimeout = 10 * time.Second + if settings.HasSetting(config.LogonTimeout) { + + timeout, err := settings.IntSetting(config.LogonTimeout) + if err != nil { + return err + } + + if timeout <= 0 { + return errors.New("LogonTimeout must be greater than zero") + } + + session.LogonTimeout = time.Duration(timeout) * time.Second + } + return f.configureSocketConnectAddress(session, settings) } diff --git a/session_factory_test.go b/session_factory_test.go index 7db6cfd87..63a5a004b 100644 --- a/session_factory_test.go +++ b/session_factory_test.go @@ -129,7 +129,7 @@ func (s *SessionFactorySuite) TestResendRequestChunkSize() { s.Equal(2500, session.ResendRequestChunkSize) s.SessionSettings.Set(config.ResendRequestChunkSize, "notanint") - session, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + _, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) s.NotNil(err) } @@ -353,6 +353,8 @@ func (s *SessionFactorySuite) TestNewSessionBuildInitiators() { s.True(session.InitiateLogon) s.Equal(34*time.Second, session.HeartBtInt) s.Equal(30*time.Second, session.ReconnectInterval) + s.Equal(10*time.Second, session.LogonTimeout) + s.Equal(2*time.Second, session.LogoutTimeout) s.Equal("127.0.0.1:5000", session.SocketConnectAddress[0]) } @@ -399,6 +401,54 @@ func (s *SessionFactorySuite) TestNewSessionBuildInitiatorsValidReconnectInterva s.NotNil(err, "ReconnectInterval must be greater than zero") } +func (s *SessionFactorySuite) TestNewSessionBuildInitiatorsValidLogoutTimeout() { + s.sessionFactory.BuildInitiators = true + s.SessionSettings.Set(config.HeartBtInt, "34") + s.SessionSettings.Set(config.SocketConnectHost, "127.0.0.1") + s.SessionSettings.Set(config.SocketConnectPort, "3000") + + s.SessionSettings.Set(config.LogoutTimeout, "45") + session, err := s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + s.Nil(err) + s.Equal(45*time.Second, session.LogoutTimeout) + + s.SessionSettings.Set(config.LogoutTimeout, "not a number") + _, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + s.NotNil(err, "LogoutTimeout must be a number") + + s.SessionSettings.Set(config.LogoutTimeout, "0") + _, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + s.NotNil(err, "LogoutTimeout must be greater than zero") + + s.SessionSettings.Set(config.LogoutTimeout, "-20") + _, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + s.NotNil(err, "LogoutTimeout must be greater than zero") +} + +func (s *SessionFactorySuite) TestNewSessionBuildInitiatorsValidLogonTimeout() { + s.sessionFactory.BuildInitiators = true + s.SessionSettings.Set(config.HeartBtInt, "34") + s.SessionSettings.Set(config.SocketConnectHost, "127.0.0.1") + s.SessionSettings.Set(config.SocketConnectPort, "3000") + + s.SessionSettings.Set(config.LogonTimeout, "45") + session, err := s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + s.Nil(err) + s.Equal(45*time.Second, session.LogonTimeout) + + s.SessionSettings.Set(config.LogonTimeout, "not a number") + _, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + s.NotNil(err, "LogonTimeout must be a number") + + s.SessionSettings.Set(config.LogonTimeout, "0") + _, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + s.NotNil(err, "LogonTimeout must be greater than zero") + + s.SessionSettings.Set(config.LogonTimeout, "-20") + _, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + s.NotNil(err, "LogonTimeout must be greater than zero") +} + func (s *SessionFactorySuite) TestConfigureSocketConnectAddress() { sess := new(session) err := s.configureSocketConnectAddress(sess, s.SessionSettings) @@ -468,7 +518,7 @@ func (s *SessionFactorySuite) TestConfigureSocketConnectAddressMulti() { func (s *SessionFactorySuite) TestNewSessionTimestampPrecision() { s.SessionSettings.Set(config.TimeStampPrecision, "blah") - session, err := s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + _, err := s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) s.NotNil(err) var tests = []struct { @@ -483,7 +533,7 @@ func (s *SessionFactorySuite) TestNewSessionTimestampPrecision() { for _, test := range tests { s.SessionSettings.Set(config.TimeStampPrecision, test.config) - session, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + session, err := s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) s.Nil(err) s.Equal(session.timestampPrecision, test.precision) @@ -492,19 +542,19 @@ func (s *SessionFactorySuite) TestNewSessionTimestampPrecision() { func (s *SessionFactorySuite) TestNewSessionMaxLatency() { s.SessionSettings.Set(config.MaxLatency, "not a number") - session, err := s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + _, err := s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) s.NotNil(err, "MaxLatency must be a number") s.SessionSettings.Set(config.MaxLatency, "-20") - session, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + _, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) s.NotNil(err, "MaxLatency must be positive") s.SessionSettings.Set(config.MaxLatency, "0") - session, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + _, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) s.NotNil(err, "MaxLatency must be positive") s.SessionSettings.Set(config.MaxLatency, "20") - session, err = s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + session, err := s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) s.Nil(err) s.Equal(session.MaxLatency, 20*time.Second) } @@ -525,3 +575,20 @@ func (s *SessionFactorySuite) TestPersistMessages() { s.Equal(test.expected, session.DisableMessagePersist) } } + +func (s *SessionFactorySuite) TestSendRatePerSecond() { + s.sessionFactory.BuildInitiators = true + s.SessionSettings.Set(config.HeartBtInt, "34") + s.SessionSettings.Set(config.SocketConnectHost, "127.0.0.1") + s.SessionSettings.Set(config.SocketConnectPort, "5000") + s.SessionSettings.Set(config.SendRatePerSecond, "10") + + session, err := s.newSession(s.SessionID, s.MessageStoreFactory, s.SessionSettings, s.LogFactory, s.App) + s.Nil(err) + s.True(session.InitiateLogon) + s.Equal(34*time.Second, session.HeartBtInt) + s.Equal(30*time.Second, session.ReconnectInterval) + s.Equal(10*time.Second, session.LogonTimeout) + s.Equal(2*time.Second, session.LogoutTimeout) + s.Equal("127.0.0.1:5000", session.SocketConnectAddress[0]) +} diff --git a/session_settings_test.go b/session_settings_test.go index a28f60529..34687ae2c 100644 --- a/session_settings_test.go +++ b/session_settings_test.go @@ -1,8 +1,9 @@ package quickfix import ( - "github.com/quickfixgo/quickfix/config" "testing" + + "github.com/quickfixgo/quickfix/config" ) func TestSessionSettings_StringSettings(t *testing.T) { diff --git a/session_state.go b/session_state.go index 0d8263fe3..089380fc6 100644 --- a/session_state.go +++ b/session_state.go @@ -22,28 +22,27 @@ func (sm *stateMachine) Start(s *session) { } func (sm *stateMachine) Connect(session *session) { - if !sm.IsSessionTime() { - session.log.OnEvent("Connection outside of session time") - sm.handleDisconnectState(session) + // No special logon logic needed for FIX Acceptors. + if !session.InitiateLogon { + sm.setState(session, logonState{}) return } - if session.InitiateLogon { - if session.RefreshOnLogon { - if err := session.store.Refresh(); err != nil { - session.logError(err) - return - } - } - - session.log.OnEvent("Sending logon request") - if err := session.sendLogon(false, false); err != nil { + if session.RefreshOnLogon { + if err := session.store.Refresh(); err != nil { session.logError(err) return } } + session.log.OnEvent("Sending logon request") + if err := session.sendLogon(); err != nil { + session.logError(err) + return + } sm.setState(session, logonState{}) + // Fire logon timeout event after the pre-configured delay period. + time.AfterFunc(session.LogonTimeout, func() { session.sessionEvent <- internal.LogonTimeout }) } func (sm *stateMachine) Stop(session *session) { @@ -194,8 +193,18 @@ func handleStateError(s *session, err error) sessionState { return latentState{} } -//sessionState is the current state of the session state machine. The session state determines how the session responds to -//incoming messages, timeouts, and requests to send application messages. +const ( + SessionStateUnknown = "Unknown" + SessionStateLatentState = "Latent State" + SessionStateInSession = "In Session" + SessionStateLogonState = "Logon State" + SessionStateLogoutState = "Logout State" + SessionStateNotSessionTime = "Not session time" + SessionStateResend = "Resend" +) + +// sessionState is the current state of the session state machine. The session state determines how the session responds to +// incoming messages, timeouts, and requests to send application messages. type sessionState interface { //FixMsgIn is called by the session on incoming messages from the counter party. The return type is the next session state //following message processing diff --git a/session_test.go b/session_test.go index bd2bd18fa..906521b4f 100644 --- a/session_test.go +++ b/session_test.go @@ -2,6 +2,7 @@ package quickfix import ( "bytes" + "reflect" "testing" "time" @@ -228,6 +229,61 @@ func (s *SessionSuite) TestCheckTargetTooLow() { s.Nil(s.session.checkTargetTooLow(msg)) } +func (s *SessionSuite) TestShouldSendReset() { + var tests = []struct { + BeginString string + ResetOnLogon bool + ResetOnDisconnect bool + ResetOnLogout bool + NextSenderMsgSeqNum int + NextTargetMsgSeqNum int + Expected bool + }{ + {BeginStringFIX40, true, false, false, 1, 1, false}, //ResetSeqNumFlag not available < fix41 + + {BeginStringFIX41, true, false, false, 1, 1, true}, //session must be configured to reset on logon + {BeginStringFIX42, true, false, false, 1, 1, true}, + {BeginStringFIX43, true, false, false, 1, 1, true}, + {BeginStringFIX44, true, false, false, 1, 1, true}, + {BeginStringFIXT11, true, false, false, 1, 1, true}, + + {BeginStringFIX41, false, true, false, 1, 1, true}, //or disconnect + {BeginStringFIX42, false, true, false, 1, 1, true}, + {BeginStringFIX43, false, true, false, 1, 1, true}, + {BeginStringFIX44, false, true, false, 1, 1, true}, + {BeginStringFIXT11, false, true, false, 1, 1, true}, + + {BeginStringFIX41, false, false, true, 1, 1, true}, //or logout + {BeginStringFIX42, false, false, true, 1, 1, true}, + {BeginStringFIX43, false, false, true, 1, 1, true}, + {BeginStringFIX44, false, false, true, 1, 1, true}, + {BeginStringFIXT11, false, false, true, 1, 1, true}, + + {BeginStringFIX41, true, true, false, 1, 1, true}, //or combo + {BeginStringFIX42, false, true, true, 1, 1, true}, + {BeginStringFIX43, true, false, true, 1, 1, true}, + {BeginStringFIX44, true, true, true, 1, 1, true}, + + {BeginStringFIX41, false, false, false, 1, 1, false}, //or will not be set + + {BeginStringFIX41, true, false, false, 1, 10, false}, //session seq numbers should be reset at the time of check + {BeginStringFIX42, true, false, false, 2, 1, false}, + {BeginStringFIX43, true, false, false, 14, 100, false}, + } + + for _, test := range tests { + s.session.sessionID.BeginString = test.BeginString + s.session.ResetOnLogon = test.ResetOnLogon + s.session.ResetOnDisconnect = test.ResetOnDisconnect + s.session.ResetOnLogout = test.ResetOnLogout + + s.Require().Nil(s.MockStore.SetNextSenderMsgSeqNum(test.NextSenderMsgSeqNum)) + s.Require().Nil(s.MockStore.SetNextTargetMsgSeqNum(test.NextTargetMsgSeqNum)) + + s.Equal(s.shouldSendReset(), test.Expected) + } +} + func (s *SessionSuite) TestCheckSessionTimeNoStartTimeEndTime() { var tests = []struct { before, after sessionState @@ -851,7 +907,7 @@ func (suite *SessionSendTestSuite) TestSendDisableMessagePersist() { func (suite *SessionSendTestSuite) TestDropAndSendAdminMessage() { suite.MockApp.On("ToAdmin") - suite.Require().Nil(suite.dropAndSend(suite.Heartbeat(), false)) + suite.Require().Nil(suite.dropAndSend(suite.Heartbeat())) suite.MockApp.AssertExpectations(suite.T()) suite.MessagePersisted(suite.MockApp.lastToAdmin) @@ -868,7 +924,7 @@ func (suite *SessionSendTestSuite) TestDropAndSendDropsQueue() { suite.NoMessageSent() suite.MockApp.On("ToAdmin") - require.Nil(suite.T(), suite.dropAndSend(suite.Logon(), false)) + require.Nil(suite.T(), suite.dropAndSend(suite.Logon())) suite.MockApp.AssertExpectations(suite.T()) msg := suite.MockApp.lastToAdmin @@ -889,7 +945,8 @@ func (suite *SessionSendTestSuite) TestDropAndSendDropsQueueWithReset() { suite.NoMessageSent() suite.MockApp.On("ToAdmin") - require.Nil(suite.T(), suite.dropAndSend(suite.Logon(), true)) + suite.Require().Nil(suite.MockStore.Reset()) + require.Nil(suite.T(), suite.dropAndSend(suite.Logon())) suite.MockApp.AssertExpectations(suite.T()) msg := suite.MockApp.lastToAdmin @@ -900,3 +957,33 @@ func (suite *SessionSendTestSuite) TestDropAndSendDropsQueueWithReset() { suite.LastToAdminMessageSent() suite.NoMessageSent() } + +func TestSessionState(t *testing.T) { + type wants struct { + connected bool + loggedOn bool + } + + tests := []struct { + name string + state sessionState + want wants + }{ + {name: "latentState", state: latentState{}, want: wants{connected: false, loggedOn: false}}, + {name: "logonState", state: logonState{}, want: wants{connected: true, loggedOn: false}}, + {name: "inSession", state: inSession{}, want: wants{connected: true, loggedOn: true}}, + {name: "logoutState", state: logoutState{}, want: wants{connected: true, loggedOn: false}}, + {name: "resendState", state: resendState{}, want: wants{connected: true, loggedOn: true}}, + {name: "pendingTimeout", state: pendingTimeout{inSession{}}, want: wants{connected: true, loggedOn: true}}, + {name: "notSessionTime", state: notSessionTime{}, want: wants{connected: false, loggedOn: false}}, + } + + for _, test := range tests { + if !reflect.DeepEqual(test.state.IsConnected(), test.want.connected) { + t.Errorf("%s.IsConnected() got = %v", test.name, test.state.IsConnected()) + } + if !reflect.DeepEqual(test.state.IsLoggedOn(), test.want.loggedOn) { + t.Errorf("%s.IsLoggedOn() got = %v", test.name, test.state.IsLoggedOn()) + } + } +} diff --git a/settings.go b/settings.go index 870cf3d71..f5371b730 100644 --- a/settings.go +++ b/settings.go @@ -130,6 +130,63 @@ func ParseSettings(reader io.Reader) (*Settings, error) { return s, err } +//ParseMapSettings creates and initializes a Settings instance with config map. +func ParseMapSettings(configMap map[string]map[string]string) (*Settings, error) { + if configMap == nil { + return nil, errors.New(`configMap must not be nil`) + } + settings := NewSettings() + + sessionSettings := settings.GlobalSettings() + if defaultMap, ok := configMap[`default`]; ok { + for k, v := range defaultMap { + sessionSettings.Set(k, v) + } + } + + if sessionMap, ok := configMap[`session`]; ok { + for k, v := range sessionMap { + sessionSettings.Set(k, v) + } + + } + + _, err := settings.AddSession(sessionSettings) + if err != nil { + return nil, err + } + return settings, nil + +} + +//ParseMapSettingsV2 creates and initializes a Settings instance with globalConfig and sessionConfigs. +func ParseMapSettingsV2(globalConfig map[string]string, sessionConfigs []map[string]string) (*Settings, error) { + if globalConfig == nil || sessionConfigs == nil { + return nil, errors.New(`globalConfig and sessionConfigs must not be nil`) + } + s := NewSettings() + + // parse global settings + for k, v := range globalConfig { + s.GlobalSettings().Set(k, v) + } + + // parse session settings + for _, sessionConfig := range sessionConfigs { + sessionSetting := NewSessionSettings() + for k, v := range sessionConfig { + sessionSetting.Set(k, v) + } + + _, err := s.AddSession(sessionSetting) + if err != nil { + return nil, err + } + } + + return s, nil +} + //GlobalSettings are default setting inherited by all session settings. func (s *Settings) GlobalSettings() *SessionSettings { s.lazyInit() diff --git a/settings_test.go b/settings_test.go index b9c219760..cab120eea 100644 --- a/settings_test.go +++ b/settings_test.go @@ -1,6 +1,7 @@ package quickfix import ( + "reflect" "strings" "testing" @@ -289,7 +290,47 @@ DataDictionary=somewhere/FIX42.xml assert.Equal(t, tc.expected, actual) } } +func TestSettings_ParseSettingsMapConfig(t *testing.T) { + configMap := map[string]map[string]string{} + configMap[`default`] = map[string]string{ + `SocketConnectHost`: "127.0.0.1", + `SocketConnectPort`: "5001", + `HeartBtInt`: "5", + `SenderCompID`: "TW", + `TargetCompID`: "ISLD", + `ResetOnLogon`: "Y", + `FileLogPath`: "tmp", + } + configMap[`session`] = map[string]string{ + `BeginString`: "FIX.4.2", + } + + s, err := ParseMapSettings(configMap) + assert.Nil(t, err) + sessionSettings := s.SessionSettings()[SessionID{BeginString: "FIX.4.3", SenderCompID: "TW", TargetCompID: "ISLD"}] + assert.Nil(t, sessionSettings) + + sessionSettings = s.SessionSettings()[SessionID{BeginString: "FIX.4.2", SenderCompID: "TW", TargetCompID: "ISLD"}] + socketConnectHostVal, err := sessionSettings.Setting("SocketConnectHost") + assert.Nil(t, err) + assert.Equal(t, `127.0.0.1`, socketConnectHostVal) + socketConnectPortVal, err := sessionSettings.Setting("SocketConnectPort") + assert.Nil(t, err) + assert.Equal(t, `5001`, socketConnectPortVal) + + fileLogPathVal, err := sessionSettings.Setting("FileLogPath") + assert.Nil(t, err) + assert.Equal(t, `tmp`, fileLogPathVal) + + beginStringVal, err := sessionSettings.Setting("BeginString") + assert.Nil(t, err) + assert.Equal(t, `FIX.4.2`, beginStringVal) + + globalLogPath, err := s.GlobalSettings().Setting(config.FileLogPath) + assert.Nil(t, err) + assert.Equal(t, `tmp`, globalLogPath) +} func TestSettings_ParseSettings_WithEqualsSignInValue(t *testing.T) { s, err := ParseSettings(strings.NewReader(` [DEFAULT] @@ -371,3 +412,159 @@ func TestSettings_SessionIDFromSessionSettings(t *testing.T) { } } } + +func TestParseMapSettingsV2(t *testing.T) { + type args struct { + globalConfig map[string]string + sessionConfigs []map[string]string + } + tests := []struct { + name string + args args + want *Settings + wantErr bool + }{ + { + name: "correct globalConfig, correct one sessionConfig", + args: args{ + globalConfig: map[string]string{ + "HeartBtInt": "30", + }, + sessionConfigs: []map[string]string{ + { + "SenderCompID": "TestSender", + "TargetCompID": "TestTarget", + "BeginString": BeginStringFIX44, + }, + }, + }, + want: &Settings{ + globalSettings: &SessionSettings{ + settings: map[string]string{ + "HeartBtInt": "30", + }, + }, + sessionSettings: map[SessionID]*SessionSettings{ + { + SenderCompID: "TestSender", + TargetCompID: "TestTarget", + BeginString: BeginStringFIX44, + }: { + settings: map[string]string{ + "SenderCompID": "TestSender", + "TargetCompID": "TestTarget", + "BeginString": BeginStringFIX44, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "correct globalConfig, correct multiple sessionConfigs", + args: args{ + globalConfig: map[string]string{ + "HeartBtInt": "30", + }, + sessionConfigs: []map[string]string{ + { + "SenderCompID": "TestSender1", + "TargetCompID": "TestTarget1", + "BeginString": BeginStringFIX44, + }, + { + "SenderCompID": "TestSender2", + "TargetCompID": "TestTarget2", + "BeginString": BeginStringFIX42, + }, + }, + }, + want: &Settings{ + globalSettings: &SessionSettings{ + settings: map[string]string{ + "HeartBtInt": "30", + }, + }, + sessionSettings: map[SessionID]*SessionSettings{ + { + SenderCompID: "TestSender1", + TargetCompID: "TestTarget1", + BeginString: BeginStringFIX44, + }: { + settings: map[string]string{ + "SenderCompID": "TestSender1", + "TargetCompID": "TestTarget1", + "BeginString": BeginStringFIX44, + }, + }, + { + SenderCompID: "TestSender2", + TargetCompID: "TestTarget2", + BeginString: BeginStringFIX42, + }: { + settings: map[string]string{ + "SenderCompID": "TestSender2", + "TargetCompID": "TestTarget2", + "BeginString": BeginStringFIX42, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "correct globalConfig, nil sessionConfigs", + args: args{ + globalConfig: map[string]string{ + "HeartBtInt": "30", + }, + sessionConfigs: nil, + }, + want: nil, + wantErr: true, + }, + { + name: "nil globalConfig, correct sessionConfigs", + args: args{ + globalConfig: nil, + sessionConfigs: []map[string]string{ + { + "SenderCompID": "TestSender", + "TargetCompID": "TestTarget", + "BeginString": BeginStringFIX44, + }, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "correct globalConfig, invalid sessionConfigs", + args: args{ + globalConfig: map[string]string{ + "HeartBtInt": "30", + }, + sessionConfigs: []map[string]string{ + { + "SenderCompID": "TestSender", + "TargetCompID": "TestTarget", + }, + }, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseMapSettingsV2(tt.args.globalConfig, tt.args.sessionConfigs) + if (err != nil) != tt.wantErr { + t.Errorf("ParseMapSettingsV2() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseMapSettingsV2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/sqlstore.go b/sqlstore.go index b16b0a74d..751ecbd3f 100644 --- a/sqlstore.go +++ b/sqlstore.go @@ -3,9 +3,11 @@ package quickfix import ( "database/sql" "fmt" + "regexp" "time" "github.com/quickfixgo/quickfix/config" + "github.com/pkg/errors" ) type sqlStoreFactory struct { @@ -19,6 +21,27 @@ type sqlStore struct { sqlDataSourceName string sqlConnMaxLifetime time.Duration db *sql.DB + placeholder placeholderFunc +} + +type placeholderFunc func(int) string + +var rePlaceholder = regexp.MustCompile(`\?`) + +func sqlString(raw string, placeholder placeholderFunc) string { + if placeholder == nil { + return raw + } + idx := 0 + return rePlaceholder.ReplaceAllStringFunc(raw, func(s string) string { + new := placeholder(idx) + idx += 1 + return new + }) +} + +func postgresPlaceholder(i int) string { + return fmt.Sprintf("$%d", i+1) } // NewSQLStoreFactory returns a sql-based implementation of MessageStoreFactory @@ -58,7 +81,14 @@ func newSQLStore(sessionID SessionID, driver string, dataSourceName string, conn sqlDataSourceName: dataSourceName, sqlConnMaxLifetime: connMaxLifetime, } - store.cache.Reset() + if err = store.cache.Reset(); err != nil { + err = errors.Wrap(err, "cache reset") + return + } + + if store.sqlDriver == "postgres" { + store.placeholder = postgresPlaceholder + } if store.db, err = sql.Open(store.sqlDriver, store.sqlDataSourceName); err != nil { return nil, err @@ -78,10 +108,10 @@ func newSQLStore(sessionID SessionID, driver string, dataSourceName string, conn // Reset deletes the store records and sets the seqnums back to 1 func (store *sqlStore) Reset() error { s := store.sessionID - _, err := store.db.Exec(`DELETE FROM messages + _, err := store.db.Exec(sqlString(`DELETE FROM messages WHERE beginstring=? AND session_qualifier=? AND sendercompid=? AND sendersubid=? AND senderlocid=? - AND targetcompid=? AND targetsubid=? AND targetlocid=?`, + AND targetcompid=? AND targetsubid=? AND targetlocid=?`, store.placeholder), s.BeginString, s.Qualifier, s.SenderCompID, s.SenderSubID, s.SenderLocationID, s.TargetCompID, s.TargetSubID, s.TargetLocationID) @@ -93,11 +123,11 @@ func (store *sqlStore) Reset() error { return err } - _, err = store.db.Exec(`UPDATE sessions + _, err = store.db.Exec(sqlString(`UPDATE sessions SET creation_time=?, incoming_seqnum=?, outgoing_seqnum=? WHERE beginstring=? AND session_qualifier=? AND sendercompid=? AND sendersubid=? AND senderlocid=? - AND targetcompid=? AND targetsubid=? AND targetlocid=?`, + AND targetcompid=? AND targetsubid=? AND targetlocid=?`, store.placeholder), store.cache.CreationTime(), store.cache.NextTargetMsgSeqNum(), store.cache.NextSenderMsgSeqNum(), s.BeginString, s.Qualifier, s.SenderCompID, s.SenderSubID, s.SenderLocationID, @@ -114,26 +144,30 @@ func (store *sqlStore) Refresh() error { return store.populateCache() } -func (store *sqlStore) populateCache() (err error) { +func (store *sqlStore) populateCache() error { s := store.sessionID var creationTime time.Time var incomingSeqNum, outgoingSeqNum int - row := store.db.QueryRow(`SELECT creation_time, incoming_seqnum, outgoing_seqnum + row := store.db.QueryRow(sqlString(`SELECT creation_time, incoming_seqnum, outgoing_seqnum FROM sessions WHERE beginstring=? AND session_qualifier=? AND sendercompid=? AND sendersubid=? AND senderlocid=? - AND targetcompid=? AND targetsubid=? AND targetlocid=?`, + AND targetcompid=? AND targetsubid=? AND targetlocid=?`, store.placeholder), s.BeginString, s.Qualifier, s.SenderCompID, s.SenderSubID, s.SenderLocationID, s.TargetCompID, s.TargetSubID, s.TargetLocationID) - err = row.Scan(&creationTime, &incomingSeqNum, &outgoingSeqNum) + err := row.Scan(&creationTime, &incomingSeqNum, &outgoingSeqNum) // session record found, load it if err == nil { store.cache.creationTime = creationTime - store.cache.SetNextTargetMsgSeqNum(incomingSeqNum) - store.cache.SetNextSenderMsgSeqNum(outgoingSeqNum) + if err = store.cache.SetNextTargetMsgSeqNum(incomingSeqNum); err != nil { + return errors.Wrap(err, "cache set next target") + } + if err = store.cache.SetNextSenderMsgSeqNum(outgoingSeqNum); err != nil { + return errors.Wrap(err, "cache set next sender") + } return nil } @@ -143,12 +177,12 @@ func (store *sqlStore) populateCache() (err error) { } // session record not found, create it - _, err = store.db.Exec(`INSERT INTO sessions ( + _, err = store.db.Exec(sqlString(`INSERT INTO sessions ( creation_time, incoming_seqnum, outgoing_seqnum, beginstring, session_qualifier, sendercompid, sendersubid, senderlocid, targetcompid, targetsubid, targetlocid) - VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, store.placeholder), store.cache.creationTime, store.cache.NextTargetMsgSeqNum(), store.cache.NextSenderMsgSeqNum(), @@ -172,10 +206,10 @@ func (store *sqlStore) NextTargetMsgSeqNum() int { // SetNextSenderMsgSeqNum sets the next MsgSeqNum that will be sent func (store *sqlStore) SetNextSenderMsgSeqNum(next int) error { s := store.sessionID - _, err := store.db.Exec(`UPDATE sessions SET outgoing_seqnum = ? + _, err := store.db.Exec(sqlString(`UPDATE sessions SET outgoing_seqnum = ? WHERE beginstring=? AND session_qualifier=? AND sendercompid=? AND sendersubid=? AND senderlocid=? - AND targetcompid=? AND targetsubid=? AND targetlocid=?`, + AND targetcompid=? AND targetsubid=? AND targetlocid=?`, store.placeholder), next, s.BeginString, s.Qualifier, s.SenderCompID, s.SenderSubID, s.SenderLocationID, s.TargetCompID, s.TargetSubID, s.TargetLocationID) @@ -188,10 +222,10 @@ func (store *sqlStore) SetNextSenderMsgSeqNum(next int) error { // SetNextTargetMsgSeqNum sets the next MsgSeqNum that should be received func (store *sqlStore) SetNextTargetMsgSeqNum(next int) error { s := store.sessionID - _, err := store.db.Exec(`UPDATE sessions SET incoming_seqnum = ? + _, err := store.db.Exec(sqlString(`UPDATE sessions SET incoming_seqnum = ? WHERE beginstring=? AND session_qualifier=? AND sendercompid=? AND sendersubid=? AND senderlocid=? - AND targetcompid=? AND targetsubid=? AND targetlocid=?`, + AND targetcompid=? AND targetsubid=? AND targetlocid=?`, store.placeholder), next, s.BeginString, s.Qualifier, s.SenderCompID, s.SenderSubID, s.SenderLocationID, s.TargetCompID, s.TargetSubID, s.TargetLocationID) @@ -203,13 +237,17 @@ func (store *sqlStore) SetNextTargetMsgSeqNum(next int) error { // IncrNextSenderMsgSeqNum increments the next MsgSeqNum that will be sent func (store *sqlStore) IncrNextSenderMsgSeqNum() error { - store.cache.IncrNextSenderMsgSeqNum() + if err := store.cache.IncrNextSenderMsgSeqNum(); err != nil { + return errors.Wrap(err, "cache incr next") + } return store.SetNextSenderMsgSeqNum(store.cache.NextSenderMsgSeqNum()) } // IncrNextTargetMsgSeqNum increments the next MsgSeqNum that should be received func (store *sqlStore) IncrNextTargetMsgSeqNum() error { - store.cache.IncrNextTargetMsgSeqNum() + if err := store.cache.IncrNextTargetMsgSeqNum(); err != nil { + return errors.Wrap(err, "cache incr next") + } return store.SetNextTargetMsgSeqNum(store.cache.NextTargetMsgSeqNum()) } @@ -221,12 +259,12 @@ func (store *sqlStore) CreationTime() time.Time { func (store *sqlStore) SaveMessage(seqNum int, msg []byte) error { s := store.sessionID - _, err := store.db.Exec(`INSERT INTO messages ( + _, err := store.db.Exec(sqlString(`INSERT INTO messages ( msgseqnum, message, beginstring, session_qualifier, sendercompid, sendersubid, senderlocid, targetcompid, targetsubid, targetlocid) - VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, store.placeholder), seqNum, string(msg), s.BeginString, s.Qualifier, s.SenderCompID, s.SenderSubID, s.SenderLocationID, @@ -238,12 +276,12 @@ func (store *sqlStore) SaveMessage(seqNum int, msg []byte) error { func (store *sqlStore) GetMessages(beginSeqNum, endSeqNum int) ([][]byte, error) { s := store.sessionID var msgs [][]byte - rows, err := store.db.Query(`SELECT message FROM messages + rows, err := store.db.Query(sqlString(`SELECT message FROM messages WHERE beginstring=? AND session_qualifier=? AND sendercompid=? AND sendersubid=? AND senderlocid=? AND targetcompid=? AND targetsubid=? AND targetlocid=? AND msgseqnum>=? AND msgseqnum<=? - ORDER BY msgseqnum`, + ORDER BY msgseqnum`, store.placeholder), s.BeginString, s.Qualifier, s.SenderCompID, s.SenderSubID, s.SenderLocationID, s.TargetCompID, s.TargetSubID, s.TargetLocationID, diff --git a/sqlstore_test.go b/sqlstore_test.go index b252b2c25..da2c8a5a4 100644 --- a/sqlstore_test.go +++ b/sqlstore_test.go @@ -60,6 +60,11 @@ TargetCompID=%s`, sqlDriver, sqlDsn, sessionID.BeginString, sessionID.SenderComp require.Nil(suite.T(), err) } +func (suite *SQLStoreTestSuite) TestSqlPlaceholderReplacement() { + got := sqlString("A ? B ? C ?", postgresPlaceholder) + suite.Equal("A $1 B $2 C $3", got) +} + func (suite *SQLStoreTestSuite) TearDownTest() { suite.msgStore.Close() os.RemoveAll(suite.sqlStoreRootPath) diff --git a/store.go b/store.go index 837bbca13..41b6bc0c0 100644 --- a/store.go +++ b/store.go @@ -1,6 +1,10 @@ package quickfix -import "time" +import ( + "time" + + "github.com/pkg/errors" +) //The MessageStore interface provides methods to record and retrieve messages for resend purposes type MessageStore interface { @@ -107,7 +111,9 @@ type memoryStoreFactory struct{} func (f memoryStoreFactory) Create(sessionID SessionID) (MessageStore, error) { m := new(memoryStore) - m.Reset() + if err := m.Reset(); err != nil { + return m, errors.Wrap(err, "reset") + } return m, nil } diff --git a/store_test.go b/store_test.go index a61ccbb05..185704e52 100644 --- a/store_test.go +++ b/store_test.go @@ -30,61 +30,55 @@ func TestMemoryStoreTestSuite(t *testing.T) { suite.Run(t, new(MemoryStoreTestSuite)) } -func (suite *MessageStoreTestSuite) TestMessageStore_SetNextMsgSeqNum_Refresh_IncrNextMsgSeqNum() { - t := suite.T() - +func (s *MessageStoreTestSuite) TestMessageStore_SetNextMsgSeqNum_Refresh_IncrNextMsgSeqNum() { // Given a MessageStore with the following sender and target seqnums - suite.msgStore.SetNextSenderMsgSeqNum(867) - suite.msgStore.SetNextTargetMsgSeqNum(5309) + s.Require().Nil(s.msgStore.SetNextSenderMsgSeqNum(867)) + s.Require().Nil(s.msgStore.SetNextTargetMsgSeqNum(5309)) // When the store is refreshed from its backing store - suite.msgStore.Refresh() + s.Require().Nil(s.msgStore.Refresh()) // Then the sender and target seqnums should still be - assert.Equal(t, 867, suite.msgStore.NextSenderMsgSeqNum()) - assert.Equal(t, 5309, suite.msgStore.NextTargetMsgSeqNum()) + s.Equal(867, s.msgStore.NextSenderMsgSeqNum()) + s.Equal(5309, s.msgStore.NextTargetMsgSeqNum()) // When the sender and target seqnums are incremented - require.Nil(t, suite.msgStore.IncrNextSenderMsgSeqNum()) - require.Nil(t, suite.msgStore.IncrNextTargetMsgSeqNum()) + s.Require().Nil(s.msgStore.IncrNextSenderMsgSeqNum()) + s.Require().Nil(s.msgStore.IncrNextTargetMsgSeqNum()) // Then the sender and target seqnums should be - assert.Equal(t, 868, suite.msgStore.NextSenderMsgSeqNum()) - assert.Equal(t, 5310, suite.msgStore.NextTargetMsgSeqNum()) + s.Equal(868, s.msgStore.NextSenderMsgSeqNum()) + s.Equal(5310, s.msgStore.NextTargetMsgSeqNum()) // When the store is refreshed from its backing store - suite.msgStore.Refresh() + s.Require().Nil(s.msgStore.Refresh()) // Then the sender and target seqnums should still be - assert.Equal(t, 868, suite.msgStore.NextSenderMsgSeqNum()) - assert.Equal(t, 5310, suite.msgStore.NextTargetMsgSeqNum()) + s.Equal(868, s.msgStore.NextSenderMsgSeqNum()) + s.Equal(5310, s.msgStore.NextTargetMsgSeqNum()) } -func (suite *MessageStoreTestSuite) TestMessageStore_Reset() { - t := suite.T() - +func (s *MessageStoreTestSuite) TestMessageStore_Reset() { // Given a MessageStore with the following sender and target seqnums - suite.msgStore.SetNextSenderMsgSeqNum(1234) - suite.msgStore.SetNextTargetMsgSeqNum(5678) + s.Require().Nil(s.msgStore.SetNextSenderMsgSeqNum(1234)) + s.Require().Nil(s.msgStore.SetNextTargetMsgSeqNum(5678)) // When the store is reset - require.Nil(t, suite.msgStore.Reset()) + s.Require().Nil(s.msgStore.Reset()) // Then the sender and target seqnums should be - assert.Equal(t, 1, suite.msgStore.NextSenderMsgSeqNum()) - assert.Equal(t, 1, suite.msgStore.NextTargetMsgSeqNum()) + s.Equal(1, s.msgStore.NextSenderMsgSeqNum()) + s.Equal(1, s.msgStore.NextTargetMsgSeqNum()) // When the store is refreshed from its backing store - suite.msgStore.Refresh() + s.Require().Nil(s.msgStore.Refresh()) // Then the sender and target seqnums should still be - assert.Equal(t, 1, suite.msgStore.NextSenderMsgSeqNum()) - assert.Equal(t, 1, suite.msgStore.NextTargetMsgSeqNum()) + s.Equal(1, s.msgStore.NextSenderMsgSeqNum()) + s.Equal(1, s.msgStore.NextTargetMsgSeqNum()) } -func (suite *MessageStoreTestSuite) TestMessageStore_SaveMessage_GetMessage() { - t := suite.T() - +func (s *MessageStoreTestSuite) TestMessageStore_SaveMessage_GetMessage() { // Given the following saved messages expectedMsgsBySeqNum := map[int]string{ 1: "In the frozen land of Nador", @@ -92,31 +86,31 @@ func (suite *MessageStoreTestSuite) TestMessageStore_SaveMessage_GetMessage() { 3: "and there was much rejoicing", } for seqNum, msg := range expectedMsgsBySeqNum { - require.Nil(t, suite.msgStore.SaveMessage(seqNum, []byte(msg))) + s.Require().Nil(s.msgStore.SaveMessage(seqNum, []byte(msg))) } // When the messages are retrieved from the MessageStore - actualMsgs, err := suite.msgStore.GetMessages(1, 3) - require.Nil(t, err) + actualMsgs, err := s.msgStore.GetMessages(1, 3) + s.Require().Nil(err) // Then the messages should be - require.Len(t, actualMsgs, 3) - assert.Equal(t, expectedMsgsBySeqNum[1], string(actualMsgs[0])) - assert.Equal(t, expectedMsgsBySeqNum[2], string(actualMsgs[1])) - assert.Equal(t, expectedMsgsBySeqNum[3], string(actualMsgs[2])) + s.Require().Len(actualMsgs, 3) + s.Equal(expectedMsgsBySeqNum[1], string(actualMsgs[0])) + s.Equal(expectedMsgsBySeqNum[2], string(actualMsgs[1])) + s.Equal(expectedMsgsBySeqNum[3], string(actualMsgs[2])) // When the store is refreshed from its backing store - suite.msgStore.Refresh() + s.Require().Nil(s.msgStore.Refresh()) // And the messages are retrieved from the MessageStore - actualMsgs, err = suite.msgStore.GetMessages(1, 3) - require.Nil(t, err) + actualMsgs, err = s.msgStore.GetMessages(1, 3) + s.Require().Nil(err) // Then the messages should still be - require.Len(t, actualMsgs, 3) - assert.Equal(t, expectedMsgsBySeqNum[1], string(actualMsgs[0])) - assert.Equal(t, expectedMsgsBySeqNum[2], string(actualMsgs[1])) - assert.Equal(t, expectedMsgsBySeqNum[3], string(actualMsgs[2])) + s.Require().Len(actualMsgs, 3) + s.Equal(expectedMsgsBySeqNum[1], string(actualMsgs[0])) + s.Equal(expectedMsgsBySeqNum[2], string(actualMsgs[1])) + s.Equal(expectedMsgsBySeqNum[3], string(actualMsgs[2])) } func (suite *MessageStoreTestSuite) TestMessageStore_GetMessages_EmptyStore() { @@ -163,12 +157,12 @@ func (suite *MessageStoreTestSuite) TestMessageStore_GetMessages_VariousRanges() } } -func (suite *MessageStoreTestSuite) TestMessageStore_CreationTime() { - assert.False(suite.T(), suite.msgStore.CreationTime().IsZero()) +func (s *MessageStoreTestSuite) TestMessageStore_CreationTime() { + s.False(s.msgStore.CreationTime().IsZero()) t0 := time.Now() - suite.msgStore.Reset() + s.Require().Nil(s.msgStore.Reset()) t1 := time.Now() - require.True(suite.T(), suite.msgStore.CreationTime().After(t0)) - require.True(suite.T(), suite.msgStore.CreationTime().Before(t1)) + s.Require().True(s.msgStore.CreationTime().After(t0)) + s.Require().True(s.msgStore.CreationTime().Before(t1)) } diff --git a/tag.go b/tag.go index 800375e34..20fcda0b7 100644 --- a/tag.go +++ b/tag.go @@ -43,6 +43,7 @@ const ( tagBusinessRejectReason Tag = 380 tagSessionRejectReason Tag = 373 tagRefMsgType Tag = 372 + tagBusinessRejectRefID Tag = 379 tagRefTagID Tag = 371 tagRefSeqNum Tag = 45 tagEncryptMethod Tag = 98 diff --git a/tls.go b/tls.go index 0184fc7f4..758915657 100644 --- a/tls.go +++ b/tls.go @@ -4,63 +4,71 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "io/ioutil" + "os" "github.com/quickfixgo/quickfix/config" ) func loadTLSConfig(settings *SessionSettings) (tlsConfig *tls.Config, err error) { - insecureSkipVerify := false - if settings.HasSetting(config.SocketInsecureSkipVerify) { - insecureSkipVerify, err = settings.BoolSetting(config.SocketInsecureSkipVerify) + allowSkipClientCerts := false + if settings.HasSetting(config.SocketUseSSL) { + allowSkipClientCerts, err = settings.BoolSetting(config.SocketUseSSL) if err != nil { return } } - if !settings.HasSetting(config.SocketPrivateKeyFile) && !settings.HasSetting(config.SocketCertificateFile) { - if insecureSkipVerify { - tlsConfig = defaultTLSConfig() - tlsConfig.InsecureSkipVerify = true + var serverName string + if settings.HasSetting(config.SocketServerName) { + serverName, err = settings.Setting(config.SocketServerName) + if err != nil { + return } - return } - privateKeyFile, err := settings.Setting(config.SocketPrivateKeyFile) - if err != nil { - return + insecureSkipVerify := false + if settings.HasSetting(config.SocketInsecureSkipVerify) { + insecureSkipVerify, err = settings.BoolSetting(config.SocketInsecureSkipVerify) + if err != nil { + return + } } - certificateFile, err := settings.Setting(config.SocketCertificateFile) - if err != nil { - return + if !settings.HasSetting(config.SocketPrivateKeyFile) && !settings.HasSetting(config.SocketCertificateFile) { + if !allowSkipClientCerts { + return + } } tlsConfig = defaultTLSConfig() - tlsConfig.Certificates = make([]tls.Certificate, 1) + tlsConfig.ServerName = serverName tlsConfig.InsecureSkipVerify = insecureSkipVerify + setMinVersionExplicit(settings, tlsConfig) - minVersion := "TLS12" - if settings.HasSetting(config.SocketMinimumTLSVersion) { - minVersion, err = settings.Setting(config.SocketMinimumTLSVersion) + if settings.HasSetting(config.SocketPrivateKeyFile) || settings.HasSetting(config.SocketCertificateFile) { + + var privateKeyFile string + var certificateFile string + + privateKeyFile, err = settings.Setting(config.SocketPrivateKeyFile) if err != nil { return } - switch minVersion { - case "SSL30": - tlsConfig.MinVersion = tls.VersionSSL30 - case "TLS10": - tlsConfig.MinVersion = tls.VersionTLS10 - case "TLS11": - tlsConfig.MinVersion = tls.VersionTLS11 - case "TLS12": - tlsConfig.MinVersion = tls.VersionTLS12 + certificateFile, err = settings.Setting(config.SocketCertificateFile) + if err != nil { + return + } + + tlsConfig.Certificates = make([]tls.Certificate, 1) + + if tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(certificateFile, privateKeyFile); err != nil { + return } } - if tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(certificateFile, privateKeyFile); err != nil { - return + if !allowSkipClientCerts { + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } if !settings.HasSetting(config.SocketCAFile) { @@ -72,7 +80,7 @@ func loadTLSConfig(settings *SessionSettings) (tlsConfig *tls.Config, err error) return } - pem, err := ioutil.ReadFile(caFile) + pem, err := os.ReadFile(caFile) if err != nil { return } @@ -85,12 +93,11 @@ func loadTLSConfig(settings *SessionSettings) (tlsConfig *tls.Config, err error) tlsConfig.RootCAs = certPool tlsConfig.ClientCAs = certPool - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert return } -//defaultTLSConfig brought to you by https://github.com/gtank/cryptopasta/ +// defaultTLSConfig brought to you by https://github.com/gtank/cryptopasta/ func defaultTLSConfig() *tls.Config { return &tls.Config{ // Avoids most of the memorably-named TLS attacks @@ -104,3 +111,24 @@ func defaultTLSConfig() *tls.Config { }, } } + +func setMinVersionExplicit(settings *SessionSettings, tlsConfig *tls.Config) { + if settings.HasSetting(config.SocketMinimumTLSVersion) { + minVersion, err := settings.Setting(config.SocketMinimumTLSVersion) + if err != nil { + return + } + + switch minVersion { + case "SSL30": + //nolint:staticcheck // SA1019 min version ok + tlsConfig.MinVersion = tls.VersionSSL30 + case "TLS10": + tlsConfig.MinVersion = tls.VersionTLS10 + case "TLS11": + tlsConfig.MinVersion = tls.VersionTLS11 + case "TLS12": + tlsConfig.MinVersion = tls.VersionTLS12 + } + } +} diff --git a/tls_test.go b/tls_test.go index 322c452c5..f1f17dfbb 100644 --- a/tls_test.go +++ b/tls_test.go @@ -60,7 +60,7 @@ func (s *TLSTestSuite) TestLoadTLSNoCA() { s.Len(tlsConfig.Certificates, 1) s.Nil(tlsConfig.RootCAs) s.Nil(tlsConfig.ClientCAs) - s.Equal(tls.NoClientCert, tlsConfig.ClientAuth) + s.Equal(tls.RequireAndVerifyClientCert, tlsConfig.ClientAuth) } func (s *TLSTestSuite) TestLoadTLSWithBadCA() { @@ -87,9 +87,69 @@ func (s *TLSTestSuite) TestLoadTLSWithCA() { s.Equal(tls.RequireAndVerifyClientCert, tlsConfig.ClientAuth) } +func (s *TLSTestSuite) TestLoadTLSWithOnlyCA() { + s.settings.GlobalSettings().Set(config.SocketUseSSL, "Y") + s.settings.GlobalSettings().Set(config.SocketCAFile, s.CAFile) + + tlsConfig, err := loadTLSConfig(s.settings.GlobalSettings()) + s.Nil(err) + s.NotNil(tlsConfig) + + s.NotNil(tlsConfig.RootCAs) + s.NotNil(tlsConfig.ClientCAs) +} + +func (s *TLSTestSuite) TestLoadTLSWithoutSSLWithOnlyCA() { + s.settings.GlobalSettings().Set(config.SocketCAFile, s.CAFile) + + tlsConfig, err := loadTLSConfig(s.settings.GlobalSettings()) + s.Nil(err) + s.Nil(tlsConfig) +} + +func (s *TLSTestSuite) TestLoadTLSAllowSkipClientCerts() { + s.settings.GlobalSettings().Set(config.SocketUseSSL, "Y") + + tlsConfig, err := loadTLSConfig(s.settings.GlobalSettings()) + s.Nil(err) + s.NotNil(tlsConfig) + + s.Equal(tls.NoClientCert, tlsConfig.ClientAuth) +} + +func (s *TLSTestSuite) TestServerNameUseSSL() { + s.settings.GlobalSettings().Set(config.SocketUseSSL, "Y") + s.settings.GlobalSettings().Set(config.SocketServerName, "DummyServerNameUseSSL") + + tlsConfig, err := loadTLSConfig(s.settings.GlobalSettings()) + s.Nil(err) + s.NotNil(tlsConfig) + s.Equal("DummyServerNameUseSSL", tlsConfig.ServerName) +} + +func (s *TLSTestSuite) TestServerNameWithCerts() { + s.settings.GlobalSettings().Set(config.SocketPrivateKeyFile, s.PrivateKeyFile) + s.settings.GlobalSettings().Set(config.SocketCertificateFile, s.CertificateFile) + s.settings.GlobalSettings().Set(config.SocketServerName, "DummyServerNameWithCerts") + + tlsConfig, err := loadTLSConfig(s.settings.GlobalSettings()) + s.Nil(err) + s.NotNil(tlsConfig) + s.Equal("DummyServerNameWithCerts", tlsConfig.ServerName) +} + func (s *TLSTestSuite) TestInsecureSkipVerify() { s.settings.GlobalSettings().Set(config.SocketInsecureSkipVerify, "Y") + tlsConfig, err := loadTLSConfig(s.settings.GlobalSettings()) + s.Nil(err) + s.Nil(tlsConfig) +} + +func (s *TLSTestSuite) TestInsecureSkipVerifyWithUseSSL() { + s.settings.GlobalSettings().Set(config.SocketUseSSL, "Y") + s.settings.GlobalSettings().Set(config.SocketInsecureSkipVerify, "Y") + tlsConfig, err := loadTLSConfig(s.settings.GlobalSettings()) s.Nil(err) s.NotNil(tlsConfig) @@ -120,6 +180,7 @@ func (s *TLSTestSuite) TestMinimumTLSVersion() { s.Nil(err) s.NotNil(tlsConfig) + //nolint:staticcheck s.Equal(tlsConfig.MinVersion, uint16(tls.VersionSSL30)) // TLS10 diff --git a/validation.go b/validation.go index 92aed28d5..59edfac31 100644 --- a/validation.go +++ b/validation.go @@ -10,12 +10,14 @@ type validator interface { type validatorSettings struct { CheckFieldsOutOfOrder bool + RejectInvalidMessage bool } //Default configuration for message validation. //See http://www.quickfixengine.org/quickfix/doc/html/configuration.html. var defaultValidatorSettings = validatorSettings{ CheckFieldsOutOfOrder: true, + RejectInvalidMessage: true, } type fixValidator struct { @@ -74,12 +76,14 @@ func validateFIX(d *datadictionary.DataDictionary, settings validatorSettings, m } } - if err := validateFields(d, d, msgType, msg); err != nil { - return err - } + if settings.RejectInvalidMessage { + if err := validateFields(d, d, msgType, msg); err != nil { + return err + } - if err := validateWalk(d, d, msgType, msg); err != nil { - return err + if err := validateWalk(d, d, msgType, msg); err != nil { + return err + } } return nil @@ -112,7 +116,7 @@ func validateFIXT(transportDD, appDD *datadictionary.DataDictionary, settings va } func validateMsgType(d *datadictionary.DataDictionary, msgType string, msg *Message) MessageRejectError { - if _, validMsgType := d.Messages[msgType]; validMsgType == false { + if _, validMsgType := d.Messages[msgType]; !validMsgType { return InvalidMessageType() } return nil diff --git a/validation_test.go b/validation_test.go index 1bc7c7000..10a8a7e3f 100644 --- a/validation_test.go +++ b/validation_test.go @@ -37,6 +37,8 @@ func TestValidate(t *testing.T) { tcTagIsDefinedForMessage(), tcFieldNotFoundBody(), tcFieldNotFoundHeader(), + tcInvalidTagCheckDisabled(), + tcInvalidTagCheckEnabled(), } msg := NewMessage() @@ -378,6 +380,43 @@ func tcTagSpecifiedOutOfRequiredOrderTrailer() validateTest { } } +func tcInvalidTagCheckDisabled() validateTest { + dict, _ := datadictionary.Parse("spec/FIX40.xml") + validator := &fixValidator{dict, defaultValidatorSettings} + validator.settings.RejectInvalidMessage = false + + builder := createFIX40NewOrderSingle() + tag := Tag(9999) + builder.Body.SetField(tag, FIXString("hello")) + msgBytes := builder.build() + + return validateTest{ + TestName: "Invalid Tag Check - Disabled", + Validator: validator, + MessageBytes: msgBytes, + DoNotExpectReject: true, + } +} + +func tcInvalidTagCheckEnabled() validateTest { + dict, _ := datadictionary.Parse("spec/FIX40.xml") + validator := &fixValidator{dict, defaultValidatorSettings} + validator.settings.RejectInvalidMessage = true + + builder := createFIX40NewOrderSingle() + tag := Tag(9999) + builder.Body.SetField(tag, FIXString("hello")) + msgBytes := builder.build() + + return validateTest{ + TestName: "Invalid Tag Check - Enabled", + Validator: validator, + MessageBytes: msgBytes, + DoNotExpectReject: false, + ExpectedRefTagID: &tag, + } +} + func tcTagSpecifiedOutOfRequiredOrderDisabledHeader() validateTest { dict, _ := datadictionary.Parse("spec/FIX40.xml") validator := &fixValidator{dict, defaultValidatorSettings}