├── .travis.yml ├── LICENSE ├── README.md ├── account.go ├── account_bench_test.go ├── account_test.go ├── config.go ├── core_bench_test.go ├── core_private.go ├── core_public.go ├── core_test.go ├── diagram.png ├── logger.go ├── package.json ├── storage.go ├── storage_test.go ├── transaction_private.go ├── transaction_public.go ├── transaction_test.go ├── unit.go └── unit_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.9 5 | 6 | before_install: 7 | - go get -t -v ./... 8 | 9 | script: 10 | - go test -race -coverprofile=coverage.txt -covermode=atomic 11 | 12 | after_success: 13 | - bash <(curl -s https://codecov.io/bash) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE 2 | 3 | Copyright (c) 2018 Eduard Sesigin. All rights reserved. Contacts: 4 | The license for this software and associated documentation files (the "Software"). 5 | 6 | "Software" is available under different licensing options designed to accommodate the needs of our various users: 7 | 8 | 1) "Software" licensed under the GNU Lesser General Public License (LGPL) version 3, is appropriate for the use of "Software" 9 | provided you can comply with the terms and conditions of the GNU LGPL version 3 (or GNU GPL version 3). 10 | 2) "Software" licensed under commercial licenses is appropriate for development of proprietary/commercial software where you 11 | do not want to share any source code with third parties or otherwise cannot comply with the terms of the GNU LGPL version 3. 12 | 13 | "Software" documentation is licensed under the terms of the GNU Free Documentation License (FDL) version 1.3, 14 | as published by the Free Software Foundation. Alternatively, you may use the documentation in accordance with 15 | the terms contained in a written agreement between you and the author of the documentation. 16 | 17 | For information about selling software, contact the author of the software by e-mail 18 | 19 | DISCLAIMER 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 22 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 23 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # transaction 2 | 3 | [![Build Status](https://travis-ci.org/claygod/transaction.svg?branch=master)](https://travis-ci.org/claygod/transaction) 4 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go) 5 | [![API documentation](https://godoc.org/github.com/claygod/transaction?status.svg)](https://godoc.org/github.com/claygod/transaction) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/claygod/transaction)](https://goreportcard.com/report/github.com/claygod/transaction) 7 | 8 | Embedded transactional database of accounts, running in multithreaded mode. Coverage 92.8% 9 | 10 | The library operates only with integers. If you want to work with hundredths (for example, cents in dollars), multiply everything by 100. For example, a dollar and a half, it will be 150. 11 | Limit on the maximum account size: 2 to 63 degrees (9,223,372,036,854,775,807). For example: on the account cannot be more than $92,233,720,368,547,758.07 12 | 13 | The library works in parallel mode and can process millions of requests per second. 14 | Parallel requests to the same account should not lead to an erroneous change in the balance of this account. 15 | Debit and credit with the account can be done ONLY as part of the transaction. 16 | 17 | The library has two main entities: a unit and an account. 18 | 19 | ### Unit 20 | 21 | - A unit can be a customer, a company, etc. 22 | - A unit can have many accounts (accounts are called a string variable) 23 | - A unit cannot be deleted if at least one of its accounts is not zero 24 | - If a unit receives a certain amount for a nonexistent account, such an account will be created 25 | 26 | ### Account 27 | 28 | - The account serves to account for money, shares, etc. 29 | - The account necessarily belongs to any unit. 30 | - The account belongs to only one unit. 31 | - There is only one balance on one account. 32 | - Balance is calculated only in whole numbers. 33 | 34 | ## Usage 35 | 36 | Important: in the description of methods all error return codes are written. 37 | Descriptions in the documentation: https://godoc.org/github.com/claygod/transaction 38 | The transaction has no limits on the number of credits and debits. 39 | 40 | ### Create / delete 41 | 42 | ```go 43 | tr := transaction.New() 44 | tr.Start() 45 | tr.AddUnit(123) 46 | tr.DelUnit(123) 47 | ``` 48 | 49 | ### Credit/debit of an account 50 | 51 | Credit and debit operations with the account: 52 | 53 | ```go 54 | t.Begin().Credit(id, "USD", 1).End() 55 | ``` 56 | 57 | ```go 58 | t.Begin().Debit(id, "USD", 1).End() 59 | ``` 60 | 61 | ### Transfer 62 | 63 | Example of transfer of one dollar from one account to another. 64 | 65 | ```go 66 | t.Begin(). 67 | Credit(idFrom, "USD", 1). 68 | Debit(idTo, "USD", 1). 69 | End() 70 | ``` 71 | 72 | ### Purchase / Sale 73 | 74 | A purchase is essentially two simultaneous funds transfers 75 | 76 | ```go 77 | // Example of buying two shares of "Apple" for $10 78 | tr.Begin(). 79 | Credit(buyerId, "USD", 10).Debit(sellerId, "USD", 10). 80 | Credit(sellerId, "APPLE", 2).Debit(buyerId, "APPLE", 2). 81 | End() 82 | ``` 83 | 84 | ### Save / Load 85 | 86 | ```go 87 | // Save 88 | tr := New() 89 | tr.Start() 90 | tr.AddUnit(123) 91 | tr.Begin().Debit(123, "USD", 7).End() 92 | tr.Save(path) 93 | ... 94 | ``` 95 | 96 | ```go 97 | // Load 98 | tr := New() 99 | tr.Start() 100 | tr.Load(path) 101 | tr.Begin().Credit(123, "USD", 7).End() 102 | ... 103 | ``` 104 | 105 | ### Example 106 | 107 | ```go 108 | package main 109 | 110 | import ( 111 | "fmt" 112 | 113 | tn "github.com/claygod/transaction" 114 | ) 115 | 116 | func main() { 117 | tr := tn.New() 118 | tr.Start() 119 | 120 | // add unit 121 | switch res := tr.AddUnit(123); res { 122 | case tn.Ok: 123 | fmt.Println("Done! Unit created") 124 | case tn.ErrCodeCoreCatch: 125 | fmt.Println("Not obtained permission") 126 | case tn.ErrCodeUnitExist: 127 | fmt.Println("Such a unit already exists") 128 | default: 129 | fmt.Println("Unknown error") 130 | } 131 | 132 | // transaction 133 | switch res := tr.Begin().Debit(123, "USD", 5).End(); res { 134 | case tn.Ok: 135 | fmt.Println("Done! Money added") 136 | case tn.ErrCodeUnitNotExist: 137 | fmt.Println("Unit not exist") 138 | case tn.ErrCodeTransactionCatch: 139 | fmt.Println("Account not catch") 140 | case tn.ErrCodeTransactionDebit: 141 | fmt.Println("Such a unit already exists") 142 | default: 143 | fmt.Println("Unknown error") 144 | } 145 | 146 | // save 147 | switch res := tr.Save("./test.tdb"); res { 148 | case tn.Ok: 149 | fmt.Println("Done! Data saved to file") 150 | case tn.ErrCodeCoreStop: 151 | fmt.Println("Unable to stop app") 152 | case tn.ErrCodeSaveCreateFile: 153 | fmt.Println("Could not create file") 154 | default: 155 | fmt.Println("Unknown error") 156 | } 157 | 158 | // del unit (There will be an error!) 159 | switch _, res := tr.DelUnit(123); res { 160 | case tn.Ok: 161 | fmt.Println("Done!") 162 | case tn.ErrCodeCoreCatch: 163 | fmt.Println("Not obtained permission") 164 | case tn.ErrCodeUnitExist: 165 | fmt.Println("There is no such unit") 166 | case tn.ErrCodeAccountNotStop: 167 | fmt.Println("Accounts failed to stop") 168 | case tn.ErrCodeUnitNotEmpty: 169 | fmt.Println("Accounts are not zero! You must withdraw money from the account") 170 | default: 171 | fmt.Println("Unknown error") 172 | } 173 | 174 | // transaction 175 | switch res := tr.Begin().Credit(123, "USD", 5).End(); res { 176 | case tn.Ok: 177 | fmt.Println("Done! Account cleared") 178 | case tn.ErrCodeUnitNotExist: 179 | fmt.Println("Unit not exist") 180 | case tn.ErrCodeTransactionCatch: 181 | fmt.Println("Account not catch") 182 | case tn.ErrCodeTransactionCredit: 183 | fmt.Println("Such a unit already exists") 184 | default: 185 | fmt.Println("Unknown error") 186 | } 187 | 188 | // del unit (Now it will work out!) 189 | switch _, res := tr.DelUnit(123); res { 190 | case tn.Ok: 191 | fmt.Println("Done! Now the account has been deleted") 192 | case tn.ErrCodeCoreCatch: 193 | fmt.Println("Not obtained permission") 194 | case tn.ErrCodeUnitNotExist: 195 | fmt.Println("There is no such unit") 196 | case tn.ErrCodeAccountNotStop: 197 | fmt.Println("Accounts failed to stop") 198 | case tn.ErrCodeUnitNotEmpty: 199 | fmt.Println("Accounts are not zero") 200 | default: 201 | fmt.Println(res) 202 | } 203 | } 204 | ``` 205 | 206 | Output: 207 | 208 | ``` 209 | Done! Unit created 210 | Done! Money added 211 | Done! Data saved to file 212 | Accounts are not zero! You must withdraw money from the account 213 | Done! Account cleared 214 | Done! Now the account has been deleted 215 | ``` 216 | 217 | ## Sequence diagram 218 | 219 | ![Sequence diagram](./diagram.png) 220 | 221 | ## API 222 | 223 | - New 224 | - Load ("path") 225 | - Start () 226 | - AddUnit(ID) 227 | - Begin().Debit(ID, key, amount).End() 228 | - Begin().Credit(ID, key, amount).End() 229 | - TotalUnit(ID) 230 | - TotalAccount(ID, key) 231 | - DelUnit(ID) 232 | - Stop () 233 | - Save ("path") 234 | 235 | ## F.A.Q. 236 | 237 | Why can not I add or withdraw funds from the account without a transaction, because it's faster? 238 | - The user should not be able to make a transaction on his own. This reduces the risk. In addition, in the world of finance, single operations are rare. 239 | 240 | Does the performance of your library depend on the number of processor cores? 241 | - Depends on the processor (cache size, number of cores, frequency, generation), and also depends on the RAM (size and speed), the number of accounts, the type of disk (HDD / SSD) when saving and loading. 242 | 243 | I have a single-core processor, should I use your library in this case? 244 | - The performance of the library is very high, so it will not be a break in your application. However, the system block is better to upgrade ;-) 245 | 246 | 247 | ## ToDo 248 | 249 | - [x] Draw a sequence diagram 250 | - [ ] Example of using a library as a server 251 | - [ ] Write-Ahead Logging (WAL) 252 | 253 | ## Bench 254 | 255 | i7-6700T: 256 | 257 | - BenchmarkTotalUnitSequence-8 3000000 419 ns/op 258 | - BenchmarkTotalUnitParallel-8 10000000 185 ns/op 259 | - BenchmarkCreditSequence-8 5000000 311 ns/op 260 | - BenchmarkCreditParallel-8 10000000 175 ns/op 261 | - BenchmarkDebitSequence-8 5000000 314 ns/op 262 | - BenchmarkDebitParallel-8 10000000 178 ns/op 263 | - BenchmarkTransferSequence-8 3000000 417 ns/op 264 | - BenchmarkTransferParallel-8 5000000 277 ns/op 265 | - BenchmarkBuySequence-8 2000000 644 ns/op 266 | - BenchmarkBuyParallel-8 5000000 354 ns/op 267 | 268 | ## Give us a star! 269 | 270 | If you like or are using this project to learn or start your solution, please give it a star. Thank you! 271 | 272 | #### Copyright © 2017-2025 Eduard Sesigin. All rights reserved. Contacts: 273 | -------------------------------------------------------------------------------- /account.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Account 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import ( 8 | "runtime" 9 | "sync/atomic" 10 | ) 11 | 12 | /* 13 | account - keeps a balance. 14 | Account balance can not be less than zero. 15 | 16 | The counter has two tasks: 17 | counts the number of operations performed 18 | stops the account (new operations are not started) 19 | */ 20 | type account struct { 21 | counter int64 22 | balance int64 23 | } 24 | 25 | /* 26 | newAccount - create new account. 27 | */ 28 | func newAccount(amount int64) *account { 29 | return &account{balance: amount} 30 | } 31 | 32 | /* 33 | addition - add to the balance the input variable. 34 | If the result of adding the balance and the input 35 | variable is less than zero, the balance does not change. 36 | 37 | Returned result: 38 | greater than or equal to zero // OK 39 | less than zero // Error 40 | */ 41 | func (a *account) addition(amount int64) int64 { 42 | 43 | // The hidden part of the code allows you 44 | // to speed up a bit by avoiding the cycle start 45 | //b := atomic.LoadInt64(&a.balance) 46 | //nb := b + amount 47 | //if nb < 0 || atomic.CompareAndSwapInt64(&a.balance, b, nb) { 48 | // return nb 49 | //} 50 | 51 | for i := trialLimit; i > trialStop; i-- { 52 | b := atomic.LoadInt64(&a.balance) 53 | nb := b + amount 54 | 55 | if amount < 0 && nb > b { 56 | return amount 57 | } 58 | 59 | if nb < 0 || atomic.CompareAndSwapInt64(&a.balance, b, nb) { 60 | return nb 61 | } 62 | 63 | //if nb < 0 { 64 | // return nb 65 | //} 66 | //if atomic.CompareAndSwapInt64(&a.balance, b, nb) { 67 | // return nb 68 | //} 69 | runtime.Gosched() 70 | } 71 | 72 | return permitError 73 | } 74 | 75 | /* 76 | total - current account balance. 77 | */ 78 | func (a *account) total() int64 { 79 | return atomic.LoadInt64(&a.balance) 80 | } 81 | 82 | /* 83 | catch - get permission to perform operations with the account. 84 | */ 85 | func (a *account) catch() bool { 86 | if atomic.LoadInt64(&a.counter) < 0 { 87 | return false 88 | } 89 | 90 | if atomic.AddInt64(&a.counter, 1) > 0 { 91 | return true 92 | } 93 | 94 | atomic.AddInt64(&a.counter, -1) 95 | 96 | return false 97 | } 98 | 99 | /* 100 | throw - current account operation has been completed 101 | */ 102 | func (a *account) throw() { 103 | atomic.AddInt64(&a.counter, -1) 104 | } 105 | 106 | /* 107 | start - start an account. 108 | */ 109 | func (a *account) start() bool { 110 | var currentCounter int64 111 | 112 | for i := trialLimit; i > trialStop; i-- { 113 | currentCounter = atomic.LoadInt64(&a.counter) 114 | 115 | if currentCounter >= 0 { 116 | return true 117 | } 118 | // the variable `currentCounter` is expected to be `permitError` 119 | if atomic.CompareAndSwapInt64(&a.counter, permitError, 0) { 120 | return true 121 | } 122 | 123 | runtime.Gosched() 124 | } 125 | 126 | return false 127 | } 128 | 129 | /* 130 | stop - stop an account. 131 | */ 132 | func (a *account) stop() bool { 133 | var currentCounter int64 134 | 135 | for i := trialLimit; i > trialStop; i-- { 136 | currentCounter = atomic.LoadInt64(&a.counter) 137 | 138 | switch { 139 | case currentCounter == 0: 140 | if atomic.CompareAndSwapInt64(&a.counter, 0, permitError) { 141 | return true 142 | } 143 | 144 | case currentCounter > 0: 145 | atomic.CompareAndSwapInt64(&a.counter, currentCounter, currentCounter+permitError) 146 | 147 | case currentCounter == permitError: 148 | return true 149 | } 150 | 151 | runtime.Gosched() 152 | } 153 | 154 | currentCounter = atomic.LoadInt64(&a.counter) 155 | 156 | if currentCounter < 0 && currentCounter > permitError { 157 | atomic.AddInt64(&a.counter, -permitError) 158 | } 159 | 160 | return false 161 | } 162 | -------------------------------------------------------------------------------- /account_bench_test.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Account bench 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func BenchmarkAccountTotal(b *testing.B) { 12 | b.StopTimer() 13 | a := newAccount(4767567567) 14 | b.StartTimer() 15 | 16 | for i := 0; i < b.N; i++ { 17 | a.total() 18 | } 19 | } 20 | 21 | func BenchmarkAccountStartStop(b *testing.B) { 22 | b.StopTimer() 23 | a := newAccount(4767567567) 24 | b.StartTimer() 25 | 26 | for i := 0; i < b.N; i++ { 27 | a.start() 28 | a.stop() 29 | } 30 | } 31 | 32 | func BenchmarkAccountAddition(b *testing.B) { 33 | b.StopTimer() 34 | a := newAccount(1) 35 | a.start() 36 | b.StartTimer() 37 | 38 | for i := 0; i < b.N; i++ { 39 | a.addition(1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /account_test.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Account test 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestAccountAdd(t *testing.T) { 12 | a := newAccount(100) 13 | 14 | if a.addition(50) != 150 { 15 | t.Error("Error addition (The sum does not match)") 16 | } 17 | 18 | if a.addition(-200) != -50 { 19 | t.Error("Error adding (Negative balance)") 20 | } 21 | 22 | trialLimit = trialStop 23 | if a.addition(-200) != permitError { 24 | t.Error("Error adding") 25 | } 26 | 27 | trialLimit = trialLimitConst 28 | } 29 | 30 | func TestAccountAddingOverflow1(t *testing.T) { 31 | a := newAccount(-1) 32 | 33 | if res := a.addition(-0x8000000000000000); res != -0x8000000000000000 { 34 | t.Error("Overflow error (1)") 35 | t.Error(res) 36 | } 37 | } 38 | 39 | func TestAccountAddingOverflow2(t *testing.T) { 40 | a := newAccount(-0x8000000000000000) 41 | 42 | if res := a.addition(-5); res != -5 { 43 | t.Error("Overflow error (2)") 44 | t.Error(res) 45 | } 46 | } 47 | 48 | func TestAccountCredit(t *testing.T) { 49 | a := newAccount(100) 50 | 51 | if a.addition(-50) != 50 { 52 | t.Error("Account incorrectly performed a credit operation") 53 | } 54 | 55 | if a.addition(-51) >= 0 { 56 | t.Error("There must be a negative result") 57 | } 58 | 59 | if a.addition(-1) < 0 { 60 | t.Error("A positive result was expected") 61 | } 62 | 63 | if b := a.addition(-1); b != 48 { 64 | t.Error("Awaiting 48 and received:", b) 65 | } 66 | } 67 | 68 | func TestAccountDebit(t *testing.T) { 69 | a := newAccount(100) 70 | 71 | if a.addition(50) != 150 { 72 | t.Error("Account incorrectly performed a debit operation") 73 | } 74 | } 75 | 76 | func TestAccountTotal(t *testing.T) { 77 | a := newAccount(100) 78 | a.addition(-1) 79 | a.addition(1) 80 | 81 | if a.total() != 100 { 82 | t.Error("Balance error", a.total()) 83 | } 84 | } 85 | 86 | func TestAccountCath(t *testing.T) { 87 | a := newAccount(100) 88 | 89 | if !a.catch() { 90 | t.Error("Account is free, but it was not possible to catch it") 91 | } 92 | 93 | if a.catch(); a.counter != 2 { 94 | t.Error("Account counter error") 95 | } 96 | 97 | a.counter = -1 98 | 99 | if a.catch() { 100 | t.Error("There must be an answer `false`") 101 | } 102 | 103 | a.counter = 1 104 | 105 | if !a.catch() { 106 | t.Error("There must be an answer `true`") 107 | } 108 | } 109 | 110 | func TestAccountThrow(t *testing.T) { 111 | a := newAccount(100) 112 | a.catch() 113 | 114 | if a.throw(); a.counter != 0 { 115 | t.Error("Failed to decrement the counter") 116 | } 117 | } 118 | 119 | func TestAccountStart(t *testing.T) { 120 | a := newAccount(100) 121 | trialLimit = 200 122 | a.counter = -1 123 | 124 | if a.start() { 125 | t.Error("Could not launch this account") 126 | } 127 | 128 | a.counter = 1 129 | 130 | if !a.start() { 131 | t.Error("The account already has a positive counter and the function should return the `true`") 132 | } 133 | 134 | a.counter = 0 135 | 136 | if !a.start() { 137 | t.Error("The account is already running and the function should return the` true`") 138 | } 139 | 140 | a.counter = permitError 141 | 142 | if !a.start() { 143 | t.Error("Account counter is in a position that allows launch. The launch is not carried out erroneously.") 144 | } 145 | 146 | trialLimit = trialLimitConst 147 | } 148 | 149 | func TestAccountStop(t *testing.T) { 150 | a := newAccount(100) 151 | trialLimit = 200 152 | 153 | if !a.stop() { 154 | t.Error("Could not stop this account") 155 | } 156 | 157 | a.counter = 1 158 | 159 | if a.stop() { 160 | t.Error("Could not stop this account22") 161 | } 162 | 163 | trialLimit = trialLimitConst 164 | } 165 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Config 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | const trialLimitConst int = 2000000000 //29999999 8 | const trialStop int = 64 9 | const permitError int64 = -9223372036854775806 10 | const usualNumTransaction = 4 11 | const endLineSymbol string = "\n" 12 | const separatorSymbol string = ";" 13 | 14 | type errorCodes int 15 | 16 | var trialLimit = trialLimitConst 17 | 18 | // Hasp state 19 | const ( 20 | stateClosed int64 = iota 21 | stateOpen 22 | ) 23 | 24 | // No error code 25 | const ( 26 | Ok errorCodes = 200 27 | ) 28 | 29 | // Error codes 30 | const ( 31 | ErrCodeUnitExist errorCodes = 400 + iota 32 | ErrCodeUnitNotExist 33 | ErrCodeUnitNotEmpty 34 | ErrCodeAccountExist 35 | ErrCodeAccountNotExist 36 | ErrCodeAccountNotEmpty 37 | ErrCodeAccountNotStop 38 | ErrCodeTransactionFill 39 | ErrCodeTransactionCatch 40 | ErrCodeTransactionCredit 41 | ErrCodeTransactionDebit 42 | ErrCodeCoreCatch 43 | ErrCodeCoreStart 44 | ErrCodeCoreStop 45 | ErrCodeSaveCreateFile 46 | ErrCodeLoadReadFile 47 | ErrCodeLoadStrToInt64 48 | ) 49 | 50 | // Error messages 51 | const ( 52 | errMsgUnitExist string = `This unit already exists` 53 | errMsgUnitNotExist string = `This unit already not exists` 54 | errMsgUnitNotDelAll string = `Could not delete all accounts` 55 | // errMsgAccountExist string = `This account already exists` 56 | // errMsgAccountNotExist string = `This account already not exists` 57 | // errMsgAccountNotEmpty string = `Account is not empty` 58 | // errMsgAccountNotStop string = `Account does not stop` 59 | errMsgAccountNotCatch string = `Not caught account` 60 | errMsgAccountCredit string = `Credit transaction error` 61 | errMsgCoreNotCatch string = `Not caught transactor` 62 | errMsgTransactionNotFill string = `Not fill transaction` 63 | errMsgTransactionNotCatch string = `Not caught transaction` 64 | // ErrMsgTransactionLessZero string = `Not caught transaction` // 65 | errMsgCoreNotStart string = `Core does not start` 66 | errMsgCoreNotStop string = `Core does not stop` 67 | errMsgCoreNotLoad string = `Core does not load` 68 | errMsgCoreNotSave string = `Core does not save` 69 | errMsgCoreNotReadFile string = `Core does not read file` 70 | errMsgCoreNotCreateFile string = `Core does not create file` 71 | errMsgCoreParseString string = `Could not parse line` 72 | ) 73 | -------------------------------------------------------------------------------- /core_bench_test.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Bench 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import ( 8 | // "sync/atomic" 9 | "testing" 10 | ) 11 | 12 | func BenchmarkTotalUnitSequence(b *testing.B) { 13 | b.StopTimer() 14 | 15 | tr := New() 16 | tr.Start() 17 | 18 | for i := int64(0); i < 65536; i++ { 19 | tr.AddUnit(i) 20 | tr.Begin().Debit(i, "USD", 223372036854775).End() 21 | } 22 | 23 | u := 0 24 | b.StartTimer() 25 | 26 | for i := 0; i < b.N; i++ { 27 | tr.TotalUnit(int64(uint16(u))) 28 | u++ 29 | } 30 | } 31 | 32 | func BenchmarkTotalUnitParallel(b *testing.B) { 33 | b.StopTimer() 34 | 35 | tr := New() 36 | tr.Start() 37 | tr.AddUnit(1234567) 38 | tr.Begin().Debit(1234567, "USD", 223372036854775806).End() 39 | 40 | for i := int64(0); i < 65536; i++ { 41 | tr.AddUnit(i) 42 | tr.Begin().Debit(i, "USD", 223372036854775).End() 43 | } 44 | 45 | u := 0 46 | b.StartTimer() 47 | b.RunParallel(func(pb *testing.PB) { 48 | for pb.Next() { 49 | tr.TotalUnit(int64(uint16(u))) 50 | u++ 51 | } 52 | }) 53 | } 54 | 55 | func BenchmarkCreditSequence(b *testing.B) { 56 | b.StopTimer() 57 | 58 | tr := New() 59 | tr.Start() 60 | 61 | for i := int64(0); i < 65536; i++ { 62 | tr.AddUnit(i) 63 | tr.Begin().Debit(i, "USD", 223372036854775).End() 64 | } 65 | 66 | u := 0 67 | b.StartTimer() 68 | 69 | for i := 0; i < b.N; i++ { 70 | tr.Begin().Credit(int64(uint16(i)), "USD", 1).End() 71 | u++ 72 | } 73 | } 74 | 75 | func BenchmarkCreditParallel(b *testing.B) { 76 | b.StopTimer() 77 | 78 | tr := New() 79 | tr.Start() 80 | tr.AddUnit(1234567) 81 | tr.Begin().Debit(1234567, "USD", 223372036854775806).End() 82 | 83 | for i := int64(0); i < 65536; i++ { 84 | tr.AddUnit(i) 85 | tr.Begin().Debit(i, "USD", 223372036854775).End() 86 | } 87 | 88 | u := 0 89 | b.StartTimer() 90 | 91 | b.RunParallel(func(pb *testing.PB) { 92 | for pb.Next() { 93 | tr.Begin().Credit(int64(uint16(u)), "USD", 1).End() 94 | u++ 95 | } 96 | }) 97 | } 98 | 99 | func BenchmarkDebitSequence(b *testing.B) { 100 | b.StopTimer() 101 | 102 | tr := New() 103 | tr.Start() 104 | 105 | for i := int64(0); i < 65536; i++ { 106 | tr.AddUnit(i) 107 | tr.Begin().Debit(i, "USD", 1).End() 108 | } 109 | 110 | b.StartTimer() 111 | 112 | for i := 0; i < b.N; i++ { 113 | tr.Begin().Debit(int64(uint16(i)), "USD", 1).End() 114 | } 115 | } 116 | 117 | func BenchmarkDebitParallel(b *testing.B) { 118 | b.StopTimer() 119 | 120 | tr := New() 121 | tr.Start() 122 | tr.AddUnit(1234567) 123 | 124 | for i := int64(0); i < 65536; i++ { 125 | tr.AddUnit(i) 126 | tr.Begin().Debit(int64(uint16(i)), "USD", 1).End() 127 | } 128 | 129 | i := 0 130 | b.StartTimer() 131 | 132 | b.RunParallel(func(pb *testing.PB) { 133 | for pb.Next() { 134 | tr.Begin().Debit(int64(uint16(i)), "USD", 1).End() 135 | i++ 136 | } 137 | }) 138 | } 139 | 140 | func BenchmarkTransferSequence(b *testing.B) { 141 | b.StopTimer() 142 | 143 | tr := New() 144 | tr.Start() 145 | 146 | for i := int64(0); i < 65536; i++ { 147 | tr.AddUnit(i) 148 | tr.Begin().Debit(i, "USD", 100000000).End() 149 | } 150 | 151 | u := 0 152 | b.StartTimer() 153 | 154 | for i := 0; i < b.N; i++ { 155 | tr.Begin().Credit(int64(uint16(u)), "USD", 1).Debit(int64(uint16(u+1)), "USD", 1).End() 156 | u += 2 157 | } 158 | } 159 | 160 | func BenchmarkTransferParallel(b *testing.B) { 161 | b.StopTimer() 162 | 163 | tr := New() 164 | tr.Start() 165 | tr.AddUnit(1234567) 166 | tr.AddUnit(1234568) 167 | 168 | for i := int64(0); i < 65536; i++ { 169 | tr.AddUnit(i) 170 | tr.Begin().Debit(i, "USD", 100000000).End() 171 | } 172 | 173 | u := 0 174 | b.StartTimer() 175 | 176 | b.RunParallel(func(pb *testing.PB) { 177 | for pb.Next() { 178 | tr.Begin().Credit(int64(uint16(u)), "USD", 1).Debit(int64(uint16(u+1)), "USD", 1).End() 179 | u += 2 180 | } 181 | }) 182 | } 183 | 184 | func BenchmarkBuySequence(b *testing.B) { 185 | b.StopTimer() 186 | 187 | tr := New() 188 | tr.Start() 189 | for i := int64(0); i < 65536; i++ { 190 | tr.AddUnit(i) 191 | tr.Begin().Debit(i, "USD", 100000000).End() 192 | tr.Begin().Debit(i, "APPLE", 5000000).End() 193 | } 194 | 195 | b.StartTimer() 196 | u := 0 197 | 198 | for i := 0; i < b.N; i++ { 199 | //tn.reqs[0].account 200 | tr.Begin(). 201 | Credit(int64(uint16(u)), "USD", 10).Debit(int64(uint16(u+1)), "USD", 10). 202 | Debit(int64(uint16(u)), "APPLE", 2).Credit(int64(uint16(u+1)), "APPLE", 2). 203 | End() 204 | u += 2 205 | } 206 | } 207 | 208 | func BenchmarkBuyParallel(b *testing.B) { 209 | b.StopTimer() 210 | 211 | tr := New() 212 | tr.Start() 213 | tr.AddUnit(1234567) 214 | tr.AddUnit(1234568) 215 | 216 | for i := int64(0); i < 65536; i++ { 217 | tr.AddUnit(i) 218 | tr.Begin().Debit(i, "USD", 100000000).End() 219 | tr.Begin().Debit(i, "APPLE", 5000000).End() 220 | } 221 | 222 | u := 0 223 | b.StartTimer() 224 | 225 | b.RunParallel(func(pb *testing.PB) { 226 | for pb.Next() { 227 | tr.Begin(). 228 | Credit(int64(uint16(u)), "USD", 10).Debit(int64(uint16(u+1)), "USD", 10). 229 | Debit(int64(uint16(u)), "APPLE", 2).Credit(int64(uint16(u+1)), "APPLE", 2). 230 | End() 231 | u += 2 232 | } 233 | }) 234 | } 235 | 236 | func BenchmarkTrGetAccount2Sequence(b *testing.B) { 237 | b.StopTimer() 238 | 239 | tr := New() 240 | tr.Start() 241 | 242 | for i := int64(0); i < 65536; i++ { 243 | tr.AddUnit(i) 244 | tr.Begin().Debit(i, "USD", 100000000).End() 245 | tr.Begin().Debit(i, "APPLE", 5000000).End() 246 | } 247 | 248 | u := 0 249 | b.StartTimer() 250 | 251 | for i := 0; i < b.N; i++ { 252 | //tn.reqs[0].account 253 | tr.getAccount(int64(uint16(u)), "USD") 254 | u += 2 255 | } 256 | } 257 | 258 | func BenchmarkTrGetAccount2Parallel(b *testing.B) { 259 | b.StopTimer() 260 | 261 | tr := New() 262 | tr.Start() 263 | 264 | for i := int64(0); i < 65536; i++ { 265 | tr.AddUnit(i) 266 | tr.Begin().Debit(i, "USD", 100000000).End() 267 | tr.Begin().Debit(i, "APPLE", 5000000).End() 268 | } 269 | 270 | u := 0 271 | b.StartTimer() 272 | 273 | b.RunParallel(func(pb *testing.PB) { 274 | for pb.Next() { 275 | tr.getAccount(int64(uint16(u)), "USD") 276 | u += 2 277 | } 278 | }) 279 | } 280 | 281 | func BenchmarkUnitGetAccountSequence(b *testing.B) { 282 | b.StopTimer() 283 | 284 | un := newUnit() 285 | 286 | tr := New() 287 | tr.Start() 288 | 289 | for i := int64(0); i < 65536; i++ { 290 | un.getAccount("USD") 291 | } 292 | 293 | b.StartTimer() 294 | 295 | for i := 0; i < b.N; i++ { 296 | //tn.reqs[0].account 297 | un.getAccount("USD") 298 | } 299 | } 300 | 301 | /* 302 | 303 | func BenchmarkMapRead(b *testing.B) { 304 | b.StopTimer() 305 | m := make(map[int]bool) 306 | for i := 0; i < 70000; i++ { 307 | m[i] = true 308 | } 309 | b.StartTimer() 310 | for i := 0; i < b.N; i++ { 311 | _ = m[int(int16(i))] 312 | } 313 | } 314 | 315 | func BenchmarkMapAdd(b *testing.B) { 316 | b.StopTimer() 317 | m := make(map[uint64]bool) 318 | b.StartTimer() 319 | for i := 0; i < b.N; i++ { 320 | m[uint64(uint8(i))] = true 321 | } 322 | } 323 | 324 | func BenchmarkSliceAdd(b *testing.B) { 325 | b.StopTimer() 326 | m := make([]bool, 0, 7000000) 327 | b.StartTimer() 328 | for i := 0; i < b.N; i++ { 329 | m = append(m, true) 330 | } 331 | } 332 | 333 | func BenchmarkCAS(b *testing.B) { 334 | b.StopTimer() 335 | var m int64 = 0 336 | b.StartTimer() 337 | for i := 0; i < b.N; i++ { 338 | atomic.CompareAndSwapInt64(&m, 0, 0) 339 | } 340 | } 341 | 342 | func BenchmarkAtomicStore(b *testing.B) { 343 | b.StopTimer() 344 | var m int64 = 0 345 | b.StartTimer() 346 | for i := 0; i < b.N; i++ { 347 | atomic.StoreInt64(&m, 0) 348 | } 349 | } 350 | 351 | func BenchmarkAtomicLoad(b *testing.B) { 352 | b.StopTimer() 353 | var m int64 = 0 354 | b.StartTimer() 355 | for i := 0; i < b.N; i++ { 356 | atomic.LoadInt64(&m) 357 | } 358 | } 359 | 360 | func BenchmarkAtomicAdd(b *testing.B) { 361 | b.StopTimer() 362 | var m int64 = 0 363 | b.StartTimer() 364 | for i := 0; i < b.N; i++ { 365 | atomic.AddInt64(&m, 1) 366 | } 367 | } 368 | */ 369 | -------------------------------------------------------------------------------- /core_private.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Private 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import ( 8 | "sync/atomic" 9 | ) 10 | 11 | /* 12 | catch - obtaining a permit for an operation. 13 | If we have an open status, the counter is incremented 14 | and the `true` returns, otherwise the `false` is returned. 15 | */ 16 | func (c *Core) catch() bool { 17 | if atomic.LoadInt64(&c.hasp) == stateOpen { 18 | atomic.AddInt64(&c.counter, 1) 19 | 20 | return true 21 | } 22 | 23 | return false 24 | } 25 | 26 | /* 27 | throw - permission to conduct an operation is no longer required. 28 | Decrement of the counter. 29 | */ 30 | func (c *Core) throw() { 31 | atomic.AddInt64(&c.counter, -1) 32 | } 33 | 34 | /* 35 | getAccount - get account by ID and string key. 36 | If a unit with such an ID exists, then the account and code `Ok` is returned. 37 | 38 | Returned codes: 39 | ErrCodeUnitNotExist // a unit with such an ID does not exist 40 | Ok 41 | */ 42 | func (c *Core) getAccount(id int64, key string) (*account, errorCodes) { 43 | u := c.storage.getUnit(id) 44 | 45 | if u == nil { 46 | return nil, ErrCodeUnitNotExist 47 | } 48 | 49 | return u.getAccount(key), Ok 50 | } 51 | -------------------------------------------------------------------------------- /core_public.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Public 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io/ioutil" 11 | "os" 12 | "runtime" 13 | "strconv" 14 | "sync/atomic" 15 | ) 16 | 17 | /* 18 | Core - root application structure 19 | */ 20 | type Core struct { 21 | counter int64 22 | hasp int64 23 | storage *storage 24 | } 25 | 26 | /* 27 | New - create new core 28 | */ 29 | func New() Core { 30 | return Core{ 31 | hasp: stateOpen, 32 | storage: newStorage(), 33 | } 34 | } 35 | 36 | /* 37 | AddUnit - adding a new unit. 38 | Two units with the same identifier can not exist. 39 | 40 | Returned codes: 41 | ErrCodeCoreCatch // not obtained permission 42 | ErrCodeUnitExist // such a unit already exists 43 | Ok 44 | */ 45 | func (c *Core) AddUnit(id int64) errorCodes { 46 | if !c.catch() { 47 | log(errMsgCoreNotCatch).context("Unit", id).context("Method", "AddUnit").send() 48 | 49 | return ErrCodeCoreCatch 50 | } 51 | 52 | defer c.throw() 53 | 54 | if !c.storage.addUnit(id) { 55 | log(errMsgUnitExist).context("Unit", id).context("Method", "AddUnit").send() 56 | 57 | return ErrCodeUnitExist 58 | } 59 | 60 | return Ok 61 | } 62 | 63 | /* 64 | DelUnit - deletion of a unit. 65 | The unit will be deleted only when all its accounts are stopped and deleted. 66 | In case of an error, a list of not deleted accounts is returned. 67 | 68 | Returned codes: 69 | ErrCodeCoreCatch // not obtained permission 70 | ErrCodeUnitNotExist // there is no such unit 71 | ErrCodeAccountNotStop // accounts failed to stop 72 | ErrCodeUnitNotEmpty // accounts are not zero 73 | Ok 74 | */ 75 | func (c *Core) DelUnit(id int64) ([]string, errorCodes) { 76 | if !c.catch() { 77 | log(errMsgCoreNotCatch).context("Unit", id).context("Method", "DelUnit").send() 78 | 79 | return nil, ErrCodeCoreCatch 80 | } 81 | 82 | defer c.throw() 83 | 84 | un := c.storage.getUnit(id) 85 | 86 | if un == nil { 87 | log(errMsgUnitNotExist).context("Unit", id).context("Method", "DelUnit").send() 88 | 89 | return nil, ErrCodeUnitNotExist 90 | } 91 | 92 | if accList, err := un.delAllAccounts(); err != Ok { 93 | log(errMsgUnitNotDelAll).context("Error code", err).context("Unit", id).context("Method", "DelUnit").send() 94 | 95 | return accList, err 96 | } 97 | 98 | _, ok := c.storage.delUnit(id) 99 | 100 | if !ok { 101 | log(errMsgUnitNotExist).context("Unit", id).context("Method", "DelUnit").send() 102 | 103 | return nil, ErrCodeUnitNotExist 104 | } 105 | 106 | return nil, Ok 107 | } 108 | 109 | /* 110 | TotalUnit - statement on all accounts of the unit. 111 | The ID-balance array is returned. 112 | 113 | Returned codes: 114 | ErrCodeCoreCatch // not obtained permission 115 | ErrCodeUnitExist // there is no such unit 116 | Ok 117 | */ 118 | func (c *Core) TotalUnit(id int64) (map[string]int64, errorCodes) { 119 | if !c.catch() { 120 | log(errMsgCoreNotCatch).context("Unit", id).context("Method", "TotalUnit").send() 121 | 122 | return nil, ErrCodeCoreCatch 123 | } 124 | 125 | defer c.throw() 126 | 127 | un := c.storage.getUnit(id) 128 | 129 | if un == nil { 130 | log(errMsgUnitNotExist).context("Unit", id).context("Method", "TotalUnit").send() 131 | 132 | return nil, ErrCodeUnitNotExist 133 | } 134 | 135 | return un.total(), Ok 136 | } 137 | 138 | /* 139 | TotalAccount - account balance 140 | If an account has not been created before, 141 | it will be created with a zero balance. 142 | 143 | Returned codes: 144 | ErrCodeCoreCatch // not obtained permission 145 | ErrCodeUnitExist // there is no such unit 146 | Ok 147 | */ 148 | func (c *Core) TotalAccount(id int64, key string) (int64, errorCodes) { 149 | if !c.catch() { 150 | log(errMsgCoreNotCatch).context("Unit", id).context("Account", key).context("Method", "TotalAccount").send() 151 | 152 | return permitError, ErrCodeCoreCatch 153 | } 154 | 155 | defer c.throw() 156 | 157 | un := c.storage.getUnit(id) 158 | 159 | if un == nil { 160 | log(errMsgUnitNotExist).context("Unit", id).context("Account", key).context("Method", "TotalAccount").send() 161 | 162 | return permitError, ErrCodeUnitNotExist 163 | } 164 | 165 | return un.getAccount(key).total(), Ok 166 | } 167 | 168 | /* 169 | Start - start the application. 170 | Only after the start you can perform transactions. 171 | If the launch was successful, or the application is already running, 172 | the `true` is returned, otherwise it returns `false`. 173 | */ 174 | func (c *Core) Start() bool { 175 | for i := trialLimit; i > trialStop; i-- { 176 | if atomic.LoadInt64(&c.hasp) == stateOpen || atomic.CompareAndSwapInt64(&c.hasp, stateClosed, stateOpen) { 177 | return true 178 | } 179 | 180 | runtime.Gosched() 181 | } 182 | 183 | log(errMsgCoreNotStart).context("Method", "Start").send() 184 | 185 | return false 186 | } 187 | 188 | /* 189 | Stop - stop the application. 190 | The new transactions will not start. Old transactions are executed, 191 | and after the end of all running transactions, the answer is returned. 192 | */ 193 | func (c *Core) Stop() (bool, int64) { 194 | for i := trialLimit; i > trialStop; i-- { 195 | if atomic.LoadInt64(&c.hasp) == stateClosed { 196 | return true, stateClosed 197 | } else if atomic.LoadInt64(&c.counter) == 0 && atomic.CompareAndSwapInt64(&c.hasp, stateOpen, stateClosed) { 198 | return true, stateOpen 199 | } 200 | 201 | runtime.Gosched() 202 | } 203 | 204 | log(errMsgCoreNotStop).context("Method", "Stop").send() 205 | 206 | return false, atomic.LoadInt64(&c.hasp) 207 | } 208 | 209 | /* 210 | Load - loading data from a file. 211 | The application stops for the duration of this operation. 212 | The existing accounts are not overwritten. 213 | 214 | Returned codes: 215 | ErrCodeCoreStop // unable to stop app 216 | ErrCodeLoadReadFile // failed to download the file 217 | ErrCodeLoadStrToInt64 // parsing error 218 | Ok 219 | */ 220 | func (c *Core) Load(path string) (errorCodes, map[int64]string) { 221 | notLoad := make(map[int64]string) 222 | var hasp int64 223 | var ok bool 224 | // here it is possible to change the status of `hasp` ToDo: to fix 225 | 226 | if ok, hasp = c.Stop(); !ok { 227 | log(errMsgCoreNotStop).context("Method", "Load").send() 228 | 229 | return ErrCodeCoreStop, notLoad 230 | } 231 | 232 | defer func() { 233 | if hasp == stateOpen { 234 | c.Start() 235 | } 236 | }() 237 | 238 | bs, err := ioutil.ReadFile(path) 239 | 240 | if err != nil { 241 | log(errMsgCoreNotReadFile).context("Path", path).context("Method", "Load").send() 242 | 243 | return ErrCodeLoadReadFile, notLoad 244 | } 245 | 246 | endLine := []byte(endLineSymbol) 247 | separator := []byte(separatorSymbol) 248 | 249 | for _, str := range bytes.Split(bs, endLine) { 250 | a := bytes.Split(str, separator) 251 | 252 | if len(a) != 3 { 253 | continue 254 | } 255 | 256 | id, err := strconv.ParseInt(string(a[0]), 10, 64) 257 | 258 | if err != nil { 259 | log(errMsgCoreParseString).context("Path", path).context("String", str).context("Method", "Load").send() 260 | 261 | return ErrCodeLoadStrToInt64, notLoad 262 | } 263 | 264 | balance, err := strconv.ParseInt(string(a[1]), 10, 64) 265 | 266 | if err != nil { 267 | log(errMsgCoreParseString).context("Path", path).context("String", str).context("Method", "Load").send() 268 | 269 | return ErrCodeLoadStrToInt64, notLoad 270 | } 271 | 272 | un := c.storage.getUnit(id) 273 | 274 | if un == nil { 275 | c.storage.addUnit(id) 276 | un = c.storage.getUnit(id) 277 | } 278 | 279 | if _, ok := un.accounts[string(a[2])]; !ok { 280 | un.accounts[string(a[2])] = newAccount(balance) 281 | } else { 282 | // The existing accounts are not overwritten 283 | notLoad[id] = string(a[2]) 284 | } 285 | } 286 | 287 | return Ok, notLoad 288 | } 289 | 290 | /* 291 | Save - saving data to a file. 292 | The application stops for the duration of this operation. 293 | 294 | Returned codes: 295 | ErrCodeCoreStop // unable to stop app 296 | ErrCodeSaveCreateFile // could not create file 297 | Ok 298 | */ 299 | func (c *Core) Save(path string) errorCodes { 300 | var hasp int64 301 | var ok bool 302 | 303 | if ok, hasp = c.Stop(); !ok { 304 | log(errMsgCoreNotStop).context("Method", "Save").send() 305 | 306 | return ErrCodeCoreStop 307 | } 308 | 309 | defer func() { 310 | if hasp == stateOpen { 311 | c.Start() 312 | } 313 | }() 314 | 315 | var buf bytes.Buffer 316 | 317 | for i := uint64(0); i < storageNumber; i++ { 318 | for id, u := range c.storage[i].data { 319 | for key, balance := range u.totalUnsafe() { 320 | buf.Write([]byte(fmt.Sprintf("%d%s%d%s%s%s", id, separatorSymbol, balance, separatorSymbol, key, endLineSymbol))) 321 | } 322 | } 323 | } 324 | 325 | if ioutil.WriteFile(path, buf.Bytes(), os.FileMode(0777)) != nil { 326 | log(errMsgCoreNotCreateFile).context("Path", path).context("Method", "Save").send() 327 | 328 | return ErrCodeSaveCreateFile 329 | } 330 | 331 | return Ok 332 | } 333 | 334 | /* 335 | Begin - a new transaction is created and returned. 336 | The application stops for the duration of this operation. 337 | */ 338 | func (c *Core) Begin() *Transaction { 339 | return newTransaction(c) 340 | } 341 | -------------------------------------------------------------------------------- /core_test.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Test 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import ( 8 | "bytes" 9 | "io/ioutil" 10 | "os" 11 | "strconv" 12 | "testing" 13 | ) 14 | 15 | func TestUsage(t *testing.T) { 16 | path := "./test.tdb" 17 | tr := New() 18 | 19 | if !tr.Start() { 20 | t.Error("Now the start is possible!") 21 | } 22 | 23 | tr.AddUnit(123) 24 | tr.Begin().Debit(123, "USD", 7777).End() 25 | tr.Save(path) 26 | 27 | if _, err := tr.DelUnit(123); err == Ok { 28 | t.Error(err) 29 | } 30 | 31 | if err := tr.Begin().Debit(123, "USD", 7777).End(); err != Ok { 32 | t.Error(err) 33 | } 34 | 35 | tr.Stop() 36 | os.Remove(path) 37 | } 38 | 39 | func TestTransfer(t *testing.T) { 40 | tr := New() 41 | tr.Start() 42 | tr.AddUnit(14760464) 43 | tr.AddUnit(2674560) 44 | 45 | if err := tr.Begin().Debit(14760464, "USD", 11).End(); err != Ok { 46 | t.Error(err) 47 | } 48 | 49 | if err := tr.Begin().Debit(2674560, "USD", 7).End(); err != Ok { 50 | t.Error(err) 51 | } 52 | 53 | if err := tr.Begin().Credit(2674560, "USD", 2).End(); err != Ok { 54 | t.Error(err) 55 | } 56 | 57 | if err := tr.Begin(). 58 | Credit(2674560, "USD", 4). 59 | Debit(14760464, "USD", 4). 60 | End(); err != Ok { 61 | t.Error(err) 62 | } 63 | } 64 | 65 | func TestCoreStart(t *testing.T) { 66 | tr := New() 67 | 68 | if !tr.Start() { 69 | t.Error("Now the start is possible!") 70 | } 71 | 72 | tr.Stop() 73 | trialLimit = trialStop 74 | 75 | if tr.Start() { 76 | t.Error("Now the start is possible!") 77 | } 78 | 79 | trialLimit = trialLimitConst 80 | } 81 | 82 | func TestCoreStop(t *testing.T) { 83 | trialLimit = 200 84 | tr := New() 85 | 86 | if !tr.Start() { 87 | t.Error("Now the start is possible!") 88 | } 89 | 90 | if ok, _ := tr.Stop(); !ok { 91 | t.Error("Now the stop is possible!") 92 | } 93 | 94 | tr.Start() 95 | trialLimit = trialStop 96 | //tr.hasp = stateClosed 97 | if ok, _ := tr.Stop(); ok { 98 | t.Error("Due to the limitation of the number of iterations, stopping is impossible") 99 | } 100 | 101 | trialLimit = trialLimitConst 102 | } 103 | 104 | func TestCoreGetAccount(t *testing.T) { 105 | tr := New() 106 | 107 | if _, err := tr.getAccount(123, "USD"); err == Ok { 108 | t.Error("We must get an error!") 109 | } 110 | 111 | tr.AddUnit(123) 112 | 113 | if _, err := tr.getAccount(123, "USD"); err != Ok { 114 | t.Error("We should not get an error!") 115 | } 116 | } 117 | 118 | func TestCoreAddUnit(t *testing.T) { 119 | tr := New() 120 | tr.Start() 121 | 122 | if tr.AddUnit(123) != Ok { 123 | t.Error("Unable to add unit") 124 | } 125 | 126 | if tr.AddUnit(123) == Ok { 127 | t.Error("You can not re-add a unit") 128 | } 129 | 130 | tr.hasp = stateClosed 131 | 132 | if tr.AddUnit(456) == Ok { 133 | t.Error("Due to the blocking, it was not possible to add a new unit.") 134 | } 135 | } 136 | 137 | func TestCoreDelUnit(t *testing.T) { 138 | tr := New() 139 | tr.Start() 140 | 141 | if _, err := tr.DelUnit(123); err == Ok { 142 | t.Error("Removed non-existent unit") 143 | } 144 | 145 | tr.AddUnit(123) 146 | 147 | if _, err := tr.DelUnit(123); err != Ok { 148 | t.Error("The unit has not been deleted") 149 | } 150 | 151 | tr.AddUnit(456) 152 | tr.Begin().Debit(456, "USD", 5).End() 153 | 154 | tr.storage.getUnit(456).getAccount("USD").counter = 1 155 | 156 | trialLimit = trialStop + 100 157 | 158 | if _, err := tr.DelUnit(456); err == Ok { 159 | t.Error("The unit has not been deleted") 160 | } 161 | 162 | tr.hasp = stateClosed 163 | 164 | if _, err := tr.DelUnit(456); err == Ok { 165 | t.Error("Due to the blocking, it was not possible to del a unit.") 166 | } 167 | 168 | trialLimit = trialLimitConst 169 | } 170 | 171 | func TestCoreTotalUnit(t *testing.T) { 172 | tr := New() 173 | tr.Start() 174 | tr.AddUnit(123) 175 | tr.Begin().Debit(123, "USD", 1).End() 176 | 177 | arr, err := tr.TotalUnit(123) 178 | 179 | if err != Ok { 180 | t.Error("Failed to get information on the unit") 181 | } 182 | 183 | if b, ok := arr["USD"]; ok != true || b != 1 { 184 | t.Error("The received information on the unit is erroneous") 185 | } 186 | 187 | if _, err := tr.TotalUnit(456); err == Ok { 188 | t.Error("A unit does not exist, there must be an error") 189 | } 190 | 191 | tr.hasp = stateClosed 192 | 193 | if _, err := tr.TotalUnit(123); err != ErrCodeCoreCatch { 194 | t.Error("Resource is locked and can not allow operation") 195 | } 196 | 197 | tr.hasp = stateOpen 198 | } 199 | 200 | func TestCoreTotalAccount(t *testing.T) { 201 | tr := New() 202 | tr.Start() 203 | tr.AddUnit(123) 204 | tr.Begin().Debit(123, "USD", 1).End() 205 | 206 | balance, err := tr.TotalAccount(123, "USD") 207 | 208 | if err != Ok { 209 | t.Error("Failed to get information on the account") 210 | } 211 | 212 | if balance != 1 { 213 | t.Error("The received information on the account is erroneous") 214 | } 215 | 216 | if _, err := tr.TotalAccount(456, "USD"); err == Ok { 217 | t.Error("A account does not exist, there must be an error") 218 | } 219 | 220 | if balance, err := tr.TotalAccount(123, "EUR"); err != Ok || balance != 0 { 221 | t.Error("A account does not exist, there must be an error") 222 | } 223 | 224 | tr.hasp = stateClosed 225 | 226 | if _, err := tr.TotalAccount(123, "USD"); err != ErrCodeCoreCatch { 227 | t.Error("Resource is locked and can not allow operation") 228 | } 229 | 230 | tr.hasp = stateOpen 231 | } 232 | 233 | func TestCoreSave(t *testing.T) { 234 | path := "./test.tdb" 235 | tr := New() 236 | tr.Start() 237 | tr.AddUnit(123) 238 | tr.Begin().Debit(123, "USD", 7).End() 239 | 240 | trialLimit = trialStop 241 | tr.hasp = stateClosed 242 | 243 | if tr.Save(path) == Ok { 244 | t.Error("The lock should prevent the file from being saved") 245 | } 246 | 247 | trialLimit = trialLimitConst 248 | 249 | tr.hasp = stateOpen 250 | 251 | if tr.Save(path) != Ok { 252 | t.Error("There is no lock, saving should be successful") 253 | } 254 | 255 | endLine := []byte(endLineSymbol) 256 | separator := []byte(separatorSymbol) 257 | 258 | bs, err := ioutil.ReadFile(path) 259 | 260 | if err != nil { 261 | t.Error("Can not find saved file") 262 | } 263 | 264 | str := bytes.Split(bs, endLine)[0] 265 | a := bytes.Split(str, separator) 266 | 267 | if len(a) != 3 { 268 | t.Error("Invalid number of columns") 269 | } 270 | 271 | id, err := strconv.ParseInt(string(a[0]), 10, 64) 272 | 273 | if err != nil { 274 | t.Error("Error converting string to integer (id account)") 275 | } 276 | 277 | balance, err := strconv.ParseInt(string(a[1]), 10, 64) 278 | 279 | if err != nil { 280 | t.Error("Error converting string to integer (balance account)") 281 | } 282 | 283 | if id != 123 { 284 | t.Error("The account identifier does not match") 285 | } 286 | 287 | if balance != 7 { 288 | t.Error("The account balance does not match") 289 | } 290 | 291 | os.Remove(path) 292 | tr.Stop() 293 | } 294 | 295 | func TestCoreLoad(t *testing.T) { 296 | path := "./test.tdb" 297 | pathFake := "./testFake.tdb" 298 | tr := New() 299 | tr.Start() 300 | tr.AddUnit(123) 301 | tr.Begin().Debit(123, "USD", 7).End() 302 | tr.Save(path) 303 | tr.Stop() 304 | tr2 := New() 305 | 306 | if res, _ := tr2.Load(pathFake); res == Ok { 307 | t.Errorf("The file `%s` does not exist", pathFake) 308 | } 309 | 310 | if res, _ := tr2.Load(path); res != Ok { 311 | t.Errorf("The file `%s` does exist", pathFake) 312 | } 313 | 314 | tr2.Start() 315 | 316 | if res, _ := tr2.Load(path); res != Ok { 317 | t.Errorf("Error loading the database file (%d)", res) 318 | } 319 | 320 | balance, res := tr2.TotalAccount(123, "USD") 321 | 322 | if balance != 7 { 323 | t.Errorf("Error in account balance (%d)", balance) 324 | } 325 | 326 | if res != Ok { 327 | t.Errorf("Error in the downloaded account (%d)", res) 328 | } 329 | 330 | os.Remove(path) 331 | } 332 | -------------------------------------------------------------------------------- /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claygod/transaction/2e743d08bb99829b21b32072bdc28c3646ae44cd/diagram.png -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Logger 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import ( 8 | "fmt" 9 | lg "log" 10 | ) 11 | 12 | /* 13 | logger - prints error messages to the console 14 | */ 15 | type logger map[string]interface{} 16 | 17 | /* 18 | log - return new logger 19 | */ 20 | func log(msg string) logger { 21 | return logger{"": msg} //make(logger) 22 | } 23 | 24 | /* 25 | context - add context to the log 26 | */ 27 | func (l logger) context(k string, v interface{}) logger { 28 | l[k] = v 29 | 30 | return l 31 | } 32 | 33 | func (l logger) send() { 34 | out := "" 35 | 36 | for k, value := range l { 37 | switch v := value.(type) { 38 | case int, int64: 39 | out += fmt.Sprintf("%s: %d. ", k, v) 40 | 41 | case string: 42 | out += fmt.Sprintf("%s: %s. ", k, v) 43 | } 44 | } 45 | 46 | go lg.Print(out) 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | "scripts": { 2 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls", 3 | } -------------------------------------------------------------------------------- /storage.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Storage 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import ( 8 | "sync" 9 | ) 10 | 11 | const storageDegree uint64 = 16 12 | const storageNumber uint64 = 1 << storageDegree 13 | const storageShift uint64 = 64 - storageDegree 14 | 15 | /* 16 | storage - provides access to sections with units. 17 | The storage is executed as an array with maps to reduce the GC pauses. 18 | */ 19 | type storage [storageNumber]*section 20 | 21 | /* 22 | newStorage - create new storage 23 | */ 24 | func newStorage() *storage { 25 | var s storage 26 | //s := &[storageNumber]*section 27 | for i := uint64(0); i < storageNumber; i++ { 28 | s[i] = newSection() 29 | } 30 | 31 | return &s 32 | } 33 | 34 | func (s *storage) addUnit(id int64) bool { 35 | section := s[(uint64(id)<>storageShift] 36 | return section.addUnit(id) 37 | } 38 | 39 | func (s *storage) getUnit(id int64) *unit { 40 | return s[(uint64(id)<>storageShift].getUnit(id) 41 | } 42 | 43 | func (s *storage) delUnit(id int64) (*unit, bool) { 44 | return s[(uint64(id)<>storageShift].delUnit(id) 45 | } 46 | 47 | /* 48 | id - create an intermediate identifier from a persistent identifier. 49 | */ 50 | func (s *storage) id(id int64) uint64 { 51 | return (uint64(id) << storageShift) >> storageShift 52 | } 53 | 54 | /* 55 | section - provides access to units. 56 | Frequent operations to get a unit are executed in the read/unlocked mode. 57 | Rare operations of adding and removing a unit are executed with a lock. 58 | */ 59 | type section struct { 60 | sync.RWMutex 61 | data map[int64]*unit 62 | } 63 | 64 | /* 65 | newSection - create new section 66 | */ 67 | func newSection() *section { 68 | s := §ion{ 69 | data: make(map[int64]*unit), 70 | } 71 | 72 | return s 73 | } 74 | 75 | func (s *section) addUnit(id int64) bool { 76 | s.Lock() 77 | defer s.Unlock() 78 | 79 | if _, ok := s.data[id]; !ok { 80 | s.data[id] = newUnit() 81 | 82 | return true 83 | } 84 | 85 | return false 86 | } 87 | 88 | func (s *section) getUnit(id int64) *unit { 89 | s.RLock() 90 | 91 | if u, ok := s.data[id]; ok { 92 | s.RUnlock() 93 | 94 | return u 95 | } 96 | 97 | s.RUnlock() 98 | 99 | return nil 100 | } 101 | 102 | func (s *section) delUnit(id int64) (*unit, bool) { 103 | s.Lock() 104 | defer s.Unlock() 105 | 106 | if u, ok := s.data[id]; ok { 107 | delete(s.data, id) 108 | 109 | return u, true 110 | } 111 | 112 | return nil, false 113 | } 114 | -------------------------------------------------------------------------------- /storage_test.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Storage/sections test 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import "testing" 8 | 9 | func TestStorageNew(t *testing.T) { 10 | s := newStorage() 11 | 12 | if s == nil { 13 | t.Error("Error creating `Storage`") 14 | } 15 | 16 | if uint64(len(s)) != storageNumber { 17 | t.Error("Error in the number of sections") 18 | } 19 | } 20 | 21 | func TestStorageAdd(t *testing.T) { 22 | s := newStorage() 23 | 24 | if !s.addUnit(1) { 25 | t.Error("Error adding a unit") 26 | } 27 | 28 | if s.addUnit(1) { 29 | t.Error("Failed to add unit again") 30 | } 31 | } 32 | 33 | func TestStorageGet(t *testing.T) { 34 | s := newStorage() 35 | s.addUnit(1) 36 | 37 | if s.getUnit(1) == nil { 38 | t.Error("No unit found (has been added)") 39 | } 40 | 41 | if s.getUnit(2) != nil { 42 | t.Error("Found a non-existent unit") 43 | } 44 | } 45 | 46 | func TestStorageDel(t *testing.T) { 47 | s := newStorage() 48 | s.addUnit(1) 49 | 50 | if _, ok := s.delUnit(1); !ok { 51 | t.Error("Unable to delete unit") 52 | } 53 | 54 | if _, ok := s.delUnit(1); ok { 55 | t.Error("Repeated deletion of the same unit!") 56 | } 57 | } 58 | 59 | func TestStorageId(t *testing.T) { 60 | s := newStorage() 61 | 62 | if s.id(1) != 1 { 63 | t.Error("The lower bits are incorrectly recalculated") 64 | } 65 | 66 | if s.id(1048575) != 65535 { 67 | t.Error("Improperly recalculated high-order bits") 68 | } 69 | } 70 | 71 | func TestSectionNew(t *testing.T) { 72 | if newSection() == nil { 73 | t.Error("Error creating `Section`") 74 | } 75 | } 76 | 77 | func TestSectionAdd(t *testing.T) { 78 | s := newSection() 79 | 80 | if !s.addUnit(1) { 81 | t.Error("Error adding a unit") 82 | } 83 | 84 | if s.addUnit(1) { 85 | t.Error("Failed to add unit again") 86 | } 87 | } 88 | 89 | func TestSectionGet(t *testing.T) { 90 | s := newSection() 91 | s.addUnit(1) 92 | 93 | if s.getUnit(1) == nil { 94 | t.Error("No unit found (has been added)") 95 | } 96 | 97 | if s.getUnit(2) != nil { 98 | t.Error("Found a non-existent unit") 99 | } 100 | } 101 | 102 | func TestSectionDel(t *testing.T) { 103 | s := newSection() 104 | s.addUnit(1) 105 | 106 | if _, ok := s.delUnit(1); !ok { 107 | t.Error("Unable to delete unit") 108 | } 109 | 110 | if _, ok := s.delUnit(1); ok { 111 | t.Error("Repeated deletion of the same unit!") 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /transaction_private.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Transaction 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | /* 8 | newTransaction - create new Transaction. 9 | */ 10 | func newTransaction(c *Core) *Transaction { 11 | t := &Transaction{ 12 | core: c, 13 | up: make([]*request, 0, usualNumTransaction), 14 | reqs: make([]*request, 0, usualNumTransaction), 15 | } 16 | 17 | return t 18 | } 19 | 20 | /* 21 | exeTransaction - execution of a transaction. 22 | 23 | Returned codes: 24 | 25 | ErrCodeUnitNotExist // unit not exist 26 | ErrCodeTransactionCatch // account not catch 27 | ErrCodeTransactionCredit // such a unit already exists 28 | Ok 29 | */ 30 | func (t *Transaction) exeTransaction() errorCodes { 31 | // catch (core) 32 | if !t.core.catch() { 33 | log(errMsgCoreNotCatch).context("Method", "exeTransaction").send() 34 | 35 | return ErrCodeCoreCatch 36 | } 37 | 38 | defer t.core.throw() 39 | 40 | // fill 41 | if err := t.fill(); err != Ok { 42 | log(errMsgTransactionNotFill).context("Method", "exeTransaction").send() 43 | 44 | return err 45 | } 46 | 47 | // catch (accounts) 48 | if err := t.catch(); err != Ok { 49 | log(errMsgTransactionNotCatch).context("Method", "exeTransaction").send() 50 | 51 | return err 52 | } 53 | 54 | // addition 55 | for num, i := range t.reqs { 56 | if res := i.account.addition(i.amount); res < 0 { 57 | t.rollback(num) 58 | t.throw(len(t.reqs)) 59 | log(errMsgAccountCredit).context("Unit", i.id). 60 | context("Account", i.key).context("Amount", i.amount). 61 | context("Method", "exeTransaction").context("Wrong balance", res).send() 62 | 63 | return ErrCodeTransactionCredit 64 | } 65 | } 66 | // throw 67 | t.throw(len(t.reqs)) 68 | 69 | return Ok 70 | } 71 | 72 | /* 73 | rollback - rolled back account operations. 74 | */ 75 | func (t *Transaction) rollback(num int) { 76 | for i := 0; i < num; i++ { 77 | t.reqs[i].account.addition(-t.reqs[i].amount) 78 | } 79 | } 80 | 81 | /* 82 | fill - getting accounts in the list. 83 | 84 | Returned codes: 85 | 86 | ErrCodeUnitNotExist // unit not exist 87 | Ok 88 | */ 89 | func (t *Transaction) fill() errorCodes { 90 | for i, r := range t.reqs { 91 | a, err := t.core.getAccount(r.id, r.key) 92 | 93 | if err != Ok { 94 | // NOTE: log in method getAccount 95 | return err 96 | } 97 | 98 | t.reqs[i].account = a 99 | } 100 | 101 | return Ok 102 | } 103 | 104 | /* 105 | catch - obtaining permissions from accounts. 106 | 107 | Returned codes: 108 | 109 | ErrCodeTransactionCatch // account not allowed operation 110 | Ok 111 | */ 112 | func (t *Transaction) catch() errorCodes { 113 | for i, r := range t.reqs { 114 | if !r.account.catch() { 115 | t.throw(i) 116 | log(errMsgAccountNotCatch).context("Unit", r.id). 117 | context("Account", r.key).context("Method", "Transaction.catch"). 118 | context("Acc counter", r.account.counter). 119 | context("Acc balance", r.account.balance).send() 120 | 121 | return ErrCodeTransactionCatch 122 | } 123 | } 124 | 125 | return Ok 126 | } 127 | 128 | /* 129 | throw - remove permissions in accounts. 130 | */ 131 | func (t *Transaction) throw(num int) { 132 | for i, r := range t.reqs { 133 | if i >= num { 134 | break 135 | } 136 | 137 | r.account.throw() 138 | } 139 | } 140 | 141 | /* 142 | request - single operation data 143 | */ 144 | type request struct { 145 | id int64 146 | key string 147 | amount int64 148 | account *account 149 | } 150 | -------------------------------------------------------------------------------- /transaction_public.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Transaction 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | /* 8 | Transaction - preparation and execution of a transaction 9 | */ 10 | type Transaction struct { 11 | core *Core 12 | up []*request 13 | reqs []*request 14 | } 15 | 16 | /* 17 | Debit - add debit to transaction. 18 | 19 | Input variables: 20 | customer - ID 21 | account - account string code 22 | count - number (type "uint64" for "less-zero" safety) 23 | */ 24 | func (t *Transaction) Debit(customer int64, account string, count uint64) *Transaction { 25 | t.up = append(t.up, &request{id: customer, key: account, amount: int64(count)}) 26 | 27 | return t 28 | } 29 | 30 | /* 31 | Credit - add credit to transaction. 32 | 33 | Input variables: 34 | customer - ID 35 | account - account string code 36 | count - number (type "uint64" for "less-zero" safety) 37 | */ 38 | func (t *Transaction) Credit(customer int64, account string, count uint64) *Transaction { 39 | t.reqs = append(t.reqs, &request{id: customer, key: account, amount: -(int64(count))}) 40 | 41 | return t 42 | } 43 | 44 | /* 45 | End - complete the data preparation and proceed with the transaction. 46 | 47 | Returned codes: 48 | 49 | ErrCodeUnitNotExist // unit not exist 50 | ErrCodeTransactionCatch // account not catch 51 | ErrCodeTransactionCredit // such a unit already exists 52 | Ok 53 | */ 54 | func (t *Transaction) End() errorCodes { 55 | t.reqs = append(t.reqs, t.up...) 56 | 57 | return t.exeTransaction() 58 | } 59 | -------------------------------------------------------------------------------- /transaction_test.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Test 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestCreditPrepare(t *testing.T) { 12 | tr := New() 13 | tr.Start() 14 | tn := tr.Begin() 15 | tn = tn.Credit(123, "USD", 5) 16 | 17 | if len(tn.reqs) != 1 { 18 | t.Error("When preparing a transaction, the credit operation is lost.") 19 | } 20 | 21 | if tn.reqs[0].amount != -5 { 22 | t.Error("In the lending operation, the amount.") 23 | } 24 | 25 | if tn.reqs[0].id != 123 { 26 | t.Error("In the lending operation, the ID.") 27 | } 28 | 29 | if tn.reqs[0].key != "USD" { 30 | t.Error("In the lending operation, the name of the value.") 31 | } 32 | } 33 | 34 | func TestTransactionExe(t *testing.T) { 35 | tr := New() 36 | tr.Start() 37 | tr.AddUnit(123) 38 | 39 | tr.hasp = stateClosed 40 | 41 | if tr.Begin().Debit(123, "USD", 5).End() != ErrCodeCoreCatch { 42 | t.Error("Resource is locked and can not allow operation") 43 | } 44 | 45 | tr.hasp = stateOpen 46 | 47 | if tr.Begin().Debit(123, "USD", 5).End() != Ok { 48 | t.Error("Error executing a transaction") 49 | } 50 | 51 | tr.storage.getUnit(123).getAccount("USD").counter = -1 52 | 53 | if tr.Begin().Debit(123, "USD", 5).End() != ErrCodeTransactionCatch { 54 | t.Error("The transaction could not cath the account") 55 | } 56 | 57 | tr.storage.delUnit(123) 58 | 59 | if tr.Begin().Debit(123, "USD", 5).End() == Ok { 60 | t.Error("The requested unit does not exist") 61 | } 62 | } 63 | 64 | func TestTransactionCatch(t *testing.T) { 65 | tr := New() 66 | tr.Start() 67 | tr.AddUnit(1) 68 | tr.AddUnit(2) 69 | tr.Begin().Debit(1, "USD", 5).End() 70 | tr.Begin().Debit(2, "USD", 5).End() 71 | 72 | tn := tr.Begin().Credit(1, "USD", 1) 73 | tn.reqs[0].account = tr.storage.getUnit(1).getAccount("USD") 74 | 75 | if tn.catch() != Ok { 76 | t.Error("TestTransactionCatch") 77 | } 78 | 79 | tr.storage.getUnit(1).getAccount("USD").counter = -1 80 | 81 | tn2 := tr.Begin().Credit(1, "USD", 2) 82 | tn2.reqs[0].account = tr.storage.getUnit(1).getAccount("USD") 83 | 84 | if tn2.catch() == Ok { 85 | t.Error("TestTransactionCatch 222") 86 | } 87 | } 88 | 89 | func TestTransactionRollback(t *testing.T) { 90 | tr := New() 91 | tr.Start() 92 | tr.AddUnit(123) 93 | tr.Begin().Debit(123, "USD", 7).End() 94 | 95 | tn := newTransaction(&tr) 96 | tn.Credit(123, "USD", 1) 97 | tn.fill() 98 | //tn.catch() 99 | tn.exeTransaction() 100 | 101 | if num, _ := tr.TotalAccount(123, "USD"); num != 6 { 102 | t.Error("Credit operation is not carried out") 103 | } 104 | 105 | tn.rollback(1) 106 | 107 | if num, _ := tr.TotalAccount(123, "USD"); num != 7 { 108 | t.Error("Not rolled back") 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /unit.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Unit 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import ( 8 | "sync" 9 | ) 10 | 11 | /* 12 | unit - aggregates accounts. 13 | */ 14 | type unit struct { 15 | sync.Mutex 16 | accounts map[string]*account 17 | } 18 | 19 | /* 20 | newUnit - create new Unit. 21 | */ 22 | func newUnit() *unit { 23 | k := &unit{accounts: make(map[string]*account)} 24 | 25 | return k 26 | } 27 | 28 | /* 29 | getAccount - take account by key. 30 | If there is no such account, it will be created (with zero balance). 31 | */ 32 | func (u *unit) getAccount(key string) *account { 33 | a, ok := u.accounts[key] 34 | if !ok { 35 | u.Lock() 36 | defer u.Unlock() 37 | 38 | a, ok = u.accounts[key] 39 | 40 | if !ok { 41 | a = newAccount(0) 42 | u.accounts[key] = a 43 | } 44 | } 45 | 46 | return a 47 | } 48 | 49 | /* 50 | total - current balance of all accounts. 51 | At the time of the formation of the answer will be a lock. 52 | */ 53 | func (u *unit) total() map[string]int64 { 54 | t := make(map[string]int64) 55 | //u.Lock() 56 | for k, a := range u.accounts { 57 | t[k] = a.total() 58 | } 59 | //u.Unlock() 60 | return t 61 | } 62 | 63 | /* 64 | totalUnsafe - current balance of all accounts (fast and unsafe). 65 | Without locking. Use this method only in serial (non-parallel) mode. 66 | */ 67 | func (u *unit) totalUnsafe() map[string]int64 { 68 | t := make(map[string]int64) 69 | 70 | for k, a := range u.accounts { 71 | t[k] = a.total() 72 | } 73 | 74 | return t 75 | } 76 | 77 | /* 78 | delAccount - delete account. 79 | The account can not be deleted if it is not stopped or has a non-zero balance. 80 | */ 81 | func (u *unit) delAccount(key string) errorCodes { 82 | u.Lock() 83 | defer u.Unlock() 84 | 85 | a, ok := u.accounts[key] 86 | 87 | if !ok { 88 | return ErrCodeAccountNotExist 89 | } 90 | 91 | if a.total() != 0 { 92 | return ErrCodeAccountNotEmpty 93 | } 94 | 95 | if !a.stop() { 96 | return ErrCodeAccountNotStop 97 | } 98 | 99 | delete(u.accounts, key) 100 | 101 | return Ok 102 | } 103 | 104 | /* 105 | delAllAccounts - delete all accounts. 106 | On error, a list of non-stopped or non-empty accounts is returned. 107 | 108 | Returned codes: 109 | ErrCodeAccountNotStop // not stopped 110 | ErrCodeUnitNotEmpty // not empty 111 | Ok 112 | */ 113 | func (u *unit) delAllAccounts() ([]string, errorCodes) { 114 | u.Lock() 115 | defer u.Unlock() 116 | 117 | if notStop := u.stop(); len(notStop) != 0 { 118 | return notStop, ErrCodeAccountNotStop 119 | } 120 | 121 | if notDel := u.delStoppedAccounts(); len(notDel) != 0 { 122 | u.start() // Undeleted accounts are restarted 123 | 124 | return notDel, ErrCodeUnitNotEmpty 125 | } 126 | 127 | return nil, Ok 128 | } 129 | 130 | /* 131 | delStoppedAccounts - delete all accounts (they are stopped). 132 | Returns a list of not deleted accounts (with a non-zero balance). 133 | */ 134 | func (u *unit) delStoppedAccounts() []string { 135 | notDel := make([]string, 0, len(u.accounts)) 136 | 137 | for k, a := range u.accounts { 138 | if a.total() != 0 { 139 | notDel = append(notDel, k) 140 | } else { 141 | delete(u.accounts, k) 142 | } 143 | } 144 | 145 | return notDel 146 | } 147 | 148 | /* 149 | start - start all accounts. 150 | Returns a list of not starting accounts. 151 | */ 152 | func (u *unit) start() []string { 153 | notStart := make([]string, 0, len(u.accounts)) 154 | 155 | for k, a := range u.accounts { 156 | if !a.start() { 157 | notStart = append(notStart, k) 158 | } 159 | } 160 | 161 | return notStart 162 | } 163 | 164 | /* 165 | stop - stop all accounts. 166 | Returns a list of non-stopped accounts. 167 | */ 168 | func (u *unit) stop() []string { 169 | notStop := make([]string, 0, len(u.accounts)) 170 | 171 | for k, a := range u.accounts { 172 | if !a.stop() { 173 | notStop = append(notStop, k) 174 | } 175 | } 176 | 177 | return notStop 178 | } 179 | -------------------------------------------------------------------------------- /unit_test.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | // Core 4 | // Account test 5 | // Copyright © 2017-2018 Eduard Sesigin. All rights reserved. Contacts: 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestUnitGetAccount(t *testing.T) { 12 | u := newUnit() 13 | 14 | if u.getAccount("USD") == nil { 15 | t.Error("New account not received") 16 | } 17 | } 18 | 19 | func TestUnitTotal(t *testing.T) { 20 | u := newUnit() 21 | 22 | if len(u.total()) != 0 { 23 | t.Error("The unit has phantom accounts") 24 | } 25 | 26 | u.getAccount("USD").addition(5) 27 | all := u.total() 28 | 29 | if num, ok := all["USD"]; !ok || num != 5 { 30 | t.Error("Lost data from one of the accounts") 31 | } 32 | } 33 | 34 | func TestUnitDelAccount(t *testing.T) { 35 | u := newUnit() 36 | 37 | if u.delAccount("USD") != ErrCodeAccountNotExist { 38 | t.Error("Deleted non-existent account") 39 | } 40 | 41 | u.getAccount("USD").addition(5) 42 | 43 | if u.delAccount("USD") != ErrCodeAccountNotEmpty { 44 | t.Error("Deleted account with non-zero balance") 45 | } 46 | 47 | u.getAccount("USD").addition(-5) 48 | 49 | if u.delAccount("USD") != Ok { 50 | t.Error("Account not deleted, although it is possible") 51 | } 52 | 53 | u.getAccount("USD").counter = permitError + 1 54 | trialLimit = trialStop + 100 55 | 56 | if u.delAccount("USD") != ErrCodeAccountNotStop { 57 | t.Error("Account not deleted, although it is possible222") 58 | } 59 | 60 | trialLimit = trialLimitConst 61 | } 62 | 63 | func TestUnitDelAllAccount(t *testing.T) { 64 | u := newUnit() 65 | 66 | if lst, err := u.delAllAccounts(); len(lst) != 0 || err != Ok { 67 | t.Error("Error deleting all accounts (and they are not)") 68 | } 69 | 70 | trialLimit = trialStop + 100 71 | u.getAccount("USD").counter = 1 72 | 73 | if lst, err := u.delAllAccounts(); len(lst) != 1 || err != ErrCodeAccountNotStop { 74 | t.Error("When cleaning a unit, it removes a non-empty account") 75 | } 76 | 77 | trialLimit = trialLimitConst 78 | 79 | u.getAccount("USD").counter = 0 80 | u.getAccount("USD").addition(5) 81 | 82 | if lst, err := u.delAllAccounts(); len(lst) != 1 || err != ErrCodeUnitNotEmpty { 83 | t.Error("When cleaning a unit, it removes a non-empty account") 84 | } 85 | } 86 | 87 | func TestUnitDel(t *testing.T) { 88 | u := newUnit() 89 | u.getAccount("USD") 90 | 91 | if lst := u.delStoppedAccounts(); len(lst) != 0 { 92 | t.Error("It was not possible to delete all accounts (but this is possible)") 93 | } 94 | 95 | u.getAccount("USD").addition(5) 96 | 97 | if lst := u.delStoppedAccounts(); len(lst) == 0 { 98 | t.Error("It turned out to delete all accounts (but this is impossible)") 99 | } 100 | } 101 | 102 | func TestUnitStop(t *testing.T) { 103 | u := newUnit() 104 | u.getAccount("USD").addition(5) 105 | 106 | if lst := u.stop(); len(lst) != 0 { 107 | t.Error("I could stop all accounts (but it's impossible)") 108 | } 109 | 110 | trialLimit = trialStop + 100 111 | u.getAccount("USD").counter = 1 112 | 113 | if lst := u.stop(); len(lst) == 0 { 114 | t.Error("I could not stop all accounts (but it's possible)") 115 | } 116 | 117 | trialLimit = trialLimitConst 118 | } 119 | 120 | func TestUnitStart(t *testing.T) { 121 | u := newUnit() 122 | u.getAccount("USD").addition(5) 123 | u.stop() 124 | 125 | if lst := u.start(); len(lst) != 0 { 126 | t.Error("I could start all accounts (but it's impossible)") 127 | } 128 | 129 | u.stop() 130 | trialLimit = trialStop + 100 131 | u.getAccount("USD").counter = permitError + 1 132 | 133 | if lst := u.start(); len(lst) != 1 { 134 | t.Error("One account should not have started.") 135 | } 136 | 137 | trialLimit = trialLimitConst 138 | } 139 | --------------------------------------------------------------------------------