├── .gitignore ├── LICENSE.md ├── README.md ├── go.mod ├── go.sum ├── main.go └── main_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | gomvcc 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2024 Phil Eaton 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gomvcc 2 | 3 | A little, pedagogical implementation of mvcc and transaction isolation 4 | levels. 5 | 6 | Blog post and walkthrough is [here](https://notes.eatonphil.com/2024-05-16-mvcc.html). 7 | 8 | ```console 9 | $ go test 10 | ``` 11 | 12 | With debug logs: 13 | 14 | ```console 15 | $ go test -- --debug 16 | ``` 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gomvcc 2 | 3 | go 1.22.3 4 | 5 | require github.com/tidwall/btree v1.7.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= 2 | github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= 3 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "slices" 7 | 8 | "github.com/tidwall/btree" 9 | ) 10 | 11 | func assert(b bool, msg string) { 12 | if !b { 13 | panic(msg) 14 | } 15 | } 16 | 17 | func assertEq[C comparable](a C, b C, prefix string) { 18 | if a != b { 19 | panic(fmt.Sprintf("%s '%v' != '%v'", prefix, a, b)) 20 | } 21 | } 22 | 23 | var DEBUG = slices.Contains(os.Args, "--debug") 24 | 25 | func debug(a ...any) { 26 | if !DEBUG { 27 | return 28 | } 29 | 30 | args := append([]any{"[DEBUG]"}, a...) 31 | fmt.Println(args...) 32 | } 33 | 34 | type Value struct { 35 | txStartId uint64 36 | txEndId uint64 37 | value string 38 | } 39 | 40 | type TransactionState uint8 41 | 42 | const ( 43 | InProgressTransaction TransactionState = iota 44 | AbortedTransaction 45 | CommittedTransaction 46 | ) 47 | 48 | // Loosest isolation at the top, strictest isolation at the bottom. 49 | type Isolation uint8 50 | 51 | const ( 52 | ReadUncommittedIsolation Isolation = iota 53 | ReadCommittedIsolation 54 | RepeatableReadIsolation 55 | SnapshotIsolation 56 | SerializableIsolation 57 | ) 58 | 59 | type Transaction struct { 60 | isolation Isolation 61 | id uint64 62 | state TransactionState 63 | 64 | // Used only by Repeatable Read and stricter. 65 | inprogress btree.Set[uint64] 66 | 67 | // Used only by Snapshot Isolation and stricter. 68 | writeset btree.Set[string] 69 | readset btree.Set[string] 70 | } 71 | 72 | type Database struct { 73 | defaultIsolation Isolation 74 | store map[string][]Value 75 | transactions btree.Map[uint64, Transaction] 76 | nextTransactionId uint64 77 | } 78 | 79 | func newDatabase() Database { 80 | return Database{ 81 | defaultIsolation: ReadCommittedIsolation, 82 | store: map[string][]Value{}, 83 | // The `0` transaction id will be used to mean that 84 | // the id was not set. So all valid transaction ids 85 | // must start at 1. 86 | nextTransactionId: 1, 87 | } 88 | } 89 | 90 | func (d *Database) hasConflict(t1 *Transaction, conflictFn func(*Transaction, *Transaction) bool) bool { 91 | iter := d.transactions.Iter() 92 | 93 | // First see if there is any conflict with transactions that 94 | // were in progress when this one started. 95 | inprogressIter := t1.inprogress.Iter() 96 | for ok := inprogressIter.First(); ok; ok = inprogressIter.Next() { 97 | id := inprogressIter.Key() 98 | found := iter.Seek(id) 99 | if !found { 100 | continue 101 | } 102 | t2 := iter.Value() 103 | if t2.state == CommittedTransaction { 104 | if conflictFn(t1, &t2) { 105 | return true 106 | } 107 | } 108 | } 109 | 110 | // Then see if there is any conflict with transactions that 111 | // started and committed after this one started. 112 | for id := t1.id; id < d.nextTransactionId; id++ { 113 | found := iter.Seek(id) 114 | if !found { 115 | continue 116 | } 117 | 118 | t2 := iter.Value() 119 | if t2.state == CommittedTransaction { 120 | if conflictFn(t1, &t2) { 121 | return true 122 | } 123 | } 124 | } 125 | 126 | return false 127 | } 128 | 129 | func setsShareItem(s1 btree.Set[string], s2 btree.Set[string]) bool { 130 | s1Iter := s1.Iter() 131 | s2Iter := s2.Iter() 132 | for ok := s1Iter.First(); ok; ok = s1Iter.Next() { 133 | s1Key := s1Iter.Key() 134 | found := s2Iter.Seek(s1Key) 135 | if found { 136 | return true 137 | } 138 | } 139 | 140 | return false 141 | } 142 | 143 | func (d *Database) completeTransaction(t *Transaction, state TransactionState) error { 144 | debug("completing transaction ", t.id) 145 | 146 | if state == CommittedTransaction { 147 | // Snapshot Isolation imposes the additional constraint that 148 | // no transaction A may commit after writing any of the same 149 | // keys as transaction B has written and committed during 150 | // transaction A's life. 151 | if t.isolation == SnapshotIsolation && d.hasConflict(t, func(t1 *Transaction, t2 *Transaction) bool { 152 | return setsShareItem(t1.writeset, t2.writeset) 153 | }) { 154 | d.completeTransaction(t, AbortedTransaction) 155 | return fmt.Errorf("write-write conflict") 156 | } 157 | 158 | // Serializable Isolation imposes the additional constraint that 159 | // no transaction A may commit after reading any of the same 160 | // keys as transaction B has written and committed during 161 | // transaction A's life, or vice-versa. 162 | if t.isolation == SerializableIsolation && d.hasConflict(t, func(t1 *Transaction, t2 *Transaction) bool { 163 | return setsShareItem(t1.readset, t2.writeset) || 164 | setsShareItem(t1.writeset, t2.readset) 165 | }) { 166 | d.completeTransaction(t, AbortedTransaction) 167 | return fmt.Errorf("read-write conflict") 168 | } 169 | } 170 | 171 | // Update transactions. 172 | t.state = state 173 | d.transactions.Set(t.id, *t) 174 | 175 | return nil 176 | } 177 | 178 | func (d *Database) transactionState(txId uint64) Transaction { 179 | t, ok := d.transactions.Get(txId) 180 | assert(ok, "valid transaction") 181 | return t 182 | } 183 | 184 | func (d *Database) inprogress() btree.Set[uint64] { 185 | var ids btree.Set[uint64] 186 | iter := d.transactions.Iter() 187 | for ok := iter.First(); ok; ok = iter.Next() { 188 | if iter.Value().state == InProgressTransaction { 189 | ids.Insert(iter.Key()) 190 | } 191 | } 192 | return ids 193 | } 194 | 195 | func (d *Database) newTransaction() *Transaction { 196 | t := &Transaction{} 197 | t.isolation = d.defaultIsolation 198 | t.state = InProgressTransaction 199 | 200 | // Assign and increment transaction id. 201 | t.id = d.nextTransactionId 202 | d.nextTransactionId++ 203 | 204 | // Store all inprogress transaction ids. 205 | t.inprogress = d.inprogress() 206 | 207 | // Add this transaction to history. 208 | d.transactions.Set(t.id, *t) 209 | 210 | debug("starting transaction", t.id) 211 | 212 | return t 213 | } 214 | 215 | func (d *Database) assertValidTransaction(t *Transaction) { 216 | assert(t.id > 0, "valid id") 217 | assert(d.transactionState(t.id).state == InProgressTransaction, "in progress") 218 | } 219 | 220 | func (d *Database) isvisible(t *Transaction, value Value) bool { 221 | // Read Uncommitted means we simply read the last value 222 | // written. Even if the transaction that wrote this value has 223 | // not committed, and even if it has aborted. 224 | if t.isolation == ReadUncommittedIsolation { 225 | // We must merely make sure the value has not been 226 | // deleted. 227 | return value.txEndId == 0 228 | } 229 | 230 | // Read Committed means we are allowed to read any values that 231 | // are committed at the point in time where we read. 232 | if t.isolation == ReadCommittedIsolation { 233 | // If the value was created by a transaction that is 234 | // not committed, and not this current transaction, 235 | // it's no good. 236 | if value.txStartId != t.id && 237 | d.transactionState(value.txStartId).state != CommittedTransaction { 238 | return false 239 | } 240 | 241 | // If the value was deleted in this transaction, it's no good. 242 | if value.txEndId == t.id { 243 | return false 244 | } 245 | 246 | // Or if the value was deleted in some other committed 247 | // transaction, it's no good. 248 | if value.txEndId > 0 && 249 | d.transactionState(value.txEndId).state == CommittedTransaction { 250 | return false 251 | } 252 | 253 | // Otherwise the value is good. 254 | return true 255 | } 256 | 257 | // Repeatable Read, Snapshot Isolation, and Serializable 258 | // further restricts Read Committed so only versions from 259 | // transactions that completed before this one started are 260 | // visible. 261 | 262 | // Snapshot Isolation and Serializable will do additional 263 | // checks at commit time. 264 | assert(t.isolation == RepeatableReadIsolation || 265 | t.isolation == SnapshotIsolation || 266 | t.isolation == SerializableIsolation, "invalid isolation level") 267 | // Ignore values from transactions started after this one. 268 | if value.txStartId > t.id { 269 | return false 270 | } 271 | 272 | // Ignore values created from transactions in progress when 273 | // this one started. 274 | if t.inprogress.Contains(value.txStartId) { 275 | return false 276 | } 277 | 278 | // If the value was created by a transaction that is not 279 | // committed, and not this current transaction, it's no good. 280 | if d.transactionState(value.txStartId).state != CommittedTransaction && 281 | value.txStartId != t.id { 282 | return false 283 | } 284 | 285 | // If the value was deleted in this transaction, it's no good. 286 | if value.txEndId == t.id { 287 | return false 288 | } 289 | 290 | // Or if the value was deleted in some other committed 291 | // transaction that started before this one, it's no good. 292 | if value.txEndId < t.id && 293 | value.txEndId > 0 && 294 | d.transactionState(value.txEndId).state == CommittedTransaction && 295 | !t.inprogress.Contains(value.txEndId) { 296 | return false 297 | } 298 | 299 | return true 300 | } 301 | 302 | type Connection struct { 303 | tx *Transaction 304 | db *Database 305 | } 306 | 307 | func (c *Connection) execCommand(command string, args []string) (string, error) { 308 | debug(command, args) 309 | 310 | if command == "begin" { 311 | assertEq(c.tx, nil, "no running transactions") 312 | c.tx = c.db.newTransaction() 313 | c.db.assertValidTransaction(c.tx) 314 | return fmt.Sprintf("%d", c.tx.id), nil 315 | } 316 | 317 | if command == "abort" { 318 | c.db.assertValidTransaction(c.tx) 319 | err := c.db.completeTransaction(c.tx, AbortedTransaction) 320 | c.tx = nil 321 | return "", err 322 | } 323 | 324 | if command == "commit" { 325 | c.db.assertValidTransaction(c.tx) 326 | err := c.db.completeTransaction(c.tx, CommittedTransaction) 327 | c.tx = nil 328 | return "", err 329 | } 330 | 331 | if command == "set" || command == "delete" { 332 | c.db.assertValidTransaction(c.tx) 333 | 334 | key := args[0] 335 | 336 | // Mark all visible versions as now invalid. 337 | found := false 338 | for i := len(c.db.store[key]) - 1; i >= 0; i-- { 339 | value := &c.db.store[key][i] 340 | debug(value, c.tx, c.db.isvisible(c.tx, *value)) 341 | if c.db.isvisible(c.tx, *value) { 342 | value.txEndId = c.tx.id 343 | found = true 344 | } 345 | } 346 | if command == "delete" && !found { 347 | return "", fmt.Errorf("cannot delete key that does not exist") 348 | } 349 | 350 | c.tx.writeset.Insert(key) 351 | 352 | // And add a new version if it's a set command. 353 | if command == "set" { 354 | value := args[1] 355 | c.db.store[key] = append(c.db.store[key], Value{ 356 | txStartId: c.tx.id, 357 | txEndId: 0, 358 | value: value, 359 | }) 360 | 361 | return value, nil 362 | } 363 | 364 | // Delete ok. 365 | return "", nil 366 | } 367 | 368 | if command == "get" { 369 | c.db.assertValidTransaction(c.tx) 370 | 371 | key := args[0] 372 | 373 | c.tx.readset.Insert(key) 374 | 375 | for i := len(c.db.store[key]) - 1; i >= 0; i-- { 376 | value := c.db.store[key][i] 377 | debug(value, c.tx, c.db.isvisible(c.tx, value)) 378 | if c.db.isvisible(c.tx, value) { 379 | return value.value, nil 380 | } 381 | } 382 | 383 | return "", fmt.Errorf("cannot get key that does not exist") 384 | } 385 | 386 | return "", fmt.Errorf("no such command") 387 | } 388 | 389 | func (c *Connection) mustExecCommand(cmd string, args []string) string { 390 | res, err := c.execCommand(cmd, args) 391 | assertEq(err, nil, "unexpected error") 392 | return res 393 | } 394 | 395 | func (d *Database) newConnection() *Connection { 396 | return &Connection{ 397 | db: d, 398 | tx: nil, 399 | } 400 | } 401 | 402 | func main() { 403 | panic("unimplemented") 404 | } 405 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestReadUncommitted(t *testing.T) { 8 | database := newDatabase() 9 | database.defaultIsolation = ReadUncommittedIsolation 10 | 11 | c1 := database.newConnection() 12 | c1.mustExecCommand("begin", nil) 13 | 14 | c2 := database.newConnection() 15 | c2.mustExecCommand("begin", nil) 16 | 17 | c1.mustExecCommand("set", []string{"x", "hey"}) 18 | 19 | // Update is visible to self. 20 | res := c1.mustExecCommand("get", []string{"x"}) 21 | assertEq(res, "hey", "c1 get x") 22 | 23 | // But since read uncommitted, also available to everyone else. 24 | res = c2.mustExecCommand("get", []string{"x"}) 25 | assertEq(res, "hey", "c2 get x") 26 | 27 | // And if we delete, that should be respected. 28 | res = c1.mustExecCommand("delete", []string{"x"}) 29 | assertEq(res, "", "c1 delete x") 30 | 31 | res, err := c1.execCommand("get", []string{"x"}) 32 | assertEq(res, "", "c1 sees no x") 33 | assertEq(err.Error(), "cannot get key that does not exist", "c1 sees no x") 34 | 35 | res, err = c2.execCommand("get", []string{"x"}) 36 | assertEq(res, "", "c2 sees no x") 37 | assertEq(err.Error(), "cannot get key that does not exist", "c2 sees no x") 38 | } 39 | 40 | func TestReadCommitted(t *testing.T) { 41 | database := newDatabase() 42 | database.defaultIsolation = ReadCommittedIsolation 43 | 44 | c1 := database.newConnection() 45 | c1.mustExecCommand("begin", nil) 46 | 47 | c2 := database.newConnection() 48 | c2.mustExecCommand("begin", nil) 49 | 50 | // Local change is visible locally. 51 | c1.mustExecCommand("set", []string{"x", "hey"}) 52 | 53 | res := c1.mustExecCommand("get", []string{"x"}) 54 | assertEq(res, "hey", "c1 get x") 55 | 56 | // Update not available to this transaction since this is not 57 | // committed. 58 | res, err := c2.execCommand("get", []string{"x"}) 59 | assertEq(res, "", "c2 get x") 60 | assertEq(err.Error(), "cannot get key that does not exist", "c2 get x") 61 | 62 | c1.mustExecCommand("commit", nil) 63 | 64 | // Now that it's been committed, it's visible in c2. 65 | res = c2.mustExecCommand("get", []string{"x"}) 66 | assertEq(res, "hey", "c2 get x") 67 | 68 | c3 := database.newConnection() 69 | c3.mustExecCommand("begin", nil) 70 | 71 | // Local change is visible locally. 72 | c3.mustExecCommand("set", []string{"x", "yall"}) 73 | 74 | res = c3.mustExecCommand("get", []string{"x"}) 75 | assertEq(res, "yall", "c3 get x") 76 | 77 | // But not on the other commit, again. 78 | res = c2.mustExecCommand("get", []string{"x"}) 79 | assertEq(res, "hey", "c2 get x") 80 | 81 | c3.mustExecCommand("abort", nil) 82 | 83 | // And still not, if the other transaction aborted. 84 | res = c2.mustExecCommand("get", []string{"x"}) 85 | assertEq(res, "hey", "c2 get x") 86 | 87 | // And if we delete it, it should show up deleted locally. 88 | c2.mustExecCommand("delete", []string{"x"}) 89 | 90 | res, err = c2.execCommand("get", []string{"x"}) 91 | assertEq(res, "", "c2 get x") 92 | assertEq(err.Error(), "cannot get key that does not exist", "c2 get x") 93 | 94 | c2.mustExecCommand("commit", nil) 95 | 96 | // It should also show up as deleted in new transactions now 97 | // that it has been committed. 98 | c4 := database.newConnection() 99 | c4.mustExecCommand("begin", nil) 100 | 101 | res, err = c4.execCommand("get", []string{"x"}) 102 | assertEq(res, "", "c4 get x") 103 | assertEq(err.Error(), "cannot get key that does not exist", "c4 get x") 104 | } 105 | 106 | func TestRepeatableRead(t *testing.T) { 107 | database := newDatabase() 108 | database.defaultIsolation = RepeatableReadIsolation 109 | 110 | c1 := database.newConnection() 111 | c1.mustExecCommand("begin", nil) 112 | 113 | c2 := database.newConnection() 114 | c2.mustExecCommand("begin", nil) 115 | 116 | // Local change is visible locally. 117 | c1.mustExecCommand("set", []string{"x", "hey"}) 118 | res := c1.mustExecCommand("get", []string{"x"}) 119 | assertEq(res, "hey", "c1 get x") 120 | 121 | // Update not available to this transaction since this is not 122 | // committed. 123 | res, err := c2.execCommand("get", []string{"x"}) 124 | assertEq(res, "", "c2 get x") 125 | assertEq(err.Error(), "cannot get key that does not exist", "c2 get x") 126 | 127 | c1.mustExecCommand("commit", nil) 128 | 129 | // Even after committing, it's not visible in an existing 130 | // transaction. 131 | res, err = c2.execCommand("get", []string{"x"}) 132 | assertEq(res, "", "c2 get x") 133 | assertEq(err.Error(), "cannot get key that does not exist", "c2 get x") 134 | 135 | // But is available in a new transaction. 136 | c3 := database.newConnection() 137 | c3.mustExecCommand("begin", nil) 138 | 139 | res = c3.mustExecCommand("get", []string{"x"}) 140 | assertEq(res, "hey", "c3 get x") 141 | 142 | // Local change is visible locally. 143 | c3.mustExecCommand("set", []string{"x", "yall"}) 144 | res = c3.mustExecCommand("get", []string{"x"}) 145 | assertEq(res, "yall", "c3 get x") 146 | 147 | // But not on the other commit, again. 148 | res, err = c2.execCommand("get", []string{"x"}) 149 | assertEq(res, "", "c2 get x") 150 | assertEq(err.Error(), "cannot get key that does not exist", "c2 get x") 151 | 152 | c3.mustExecCommand("abort", nil) 153 | 154 | // And still not, regardless of abort, because it's an older 155 | // transaction. 156 | res, err = c2.execCommand("get", []string{"x"}) 157 | assertEq(res, "", "c2 get x") 158 | assertEq(err.Error(), "cannot get key that does not exist", "c2 get x") 159 | 160 | // And again still the aborted set is still not on a new 161 | // transaction. 162 | c4 := database.newConnection() 163 | res = c4.mustExecCommand("begin", nil) 164 | 165 | res = c4.mustExecCommand("get", []string{"x"}) 166 | assertEq(res, "hey", "c4 get x") 167 | 168 | c4.mustExecCommand("delete", []string{"x"}) 169 | c4.mustExecCommand("commit", nil) 170 | 171 | // But the delete is visible to new transactions now that this 172 | // has been committed. 173 | c5 := database.newConnection() 174 | res = c5.mustExecCommand("begin", nil) 175 | 176 | res, err = c5.execCommand("get", []string{"x"}) 177 | assertEq(res, "", "c5 get x") 178 | assertEq(err.Error(), "cannot get key that does not exist", "c5 get x") 179 | } 180 | 181 | func TestSnapshotIsolation_writewrite_conflict(t *testing.T) { 182 | database := newDatabase() 183 | database.defaultIsolation = SnapshotIsolation 184 | 185 | c1 := database.newConnection() 186 | c1.mustExecCommand("begin", nil) 187 | 188 | c2 := database.newConnection() 189 | c2.mustExecCommand("begin", nil) 190 | 191 | c3 := database.newConnection() 192 | c3.mustExecCommand("begin", nil) 193 | 194 | c1.mustExecCommand("set", []string{"x", "hey"}) 195 | c1.mustExecCommand("commit", nil) 196 | 197 | c2.mustExecCommand("set", []string{"x", "hey"}) 198 | 199 | res, err := c2.execCommand("commit", nil) 200 | assertEq(res, "", "c2 commit") 201 | assertEq(err.Error(), "write-write conflict", "c2 commit") 202 | 203 | // But unrelated keys cause no conflict. 204 | c3.mustExecCommand("set", []string{"y", "no conflict"}) 205 | c3.mustExecCommand("commit", nil) 206 | } 207 | 208 | func TestSerializableIsolation_readwrite_conflict(t *testing.T) { 209 | database := newDatabase() 210 | database.defaultIsolation = SerializableIsolation 211 | 212 | c1 := database.newConnection() 213 | c1.mustExecCommand("begin", nil) 214 | 215 | c2 := database.newConnection() 216 | c2.mustExecCommand("begin", nil) 217 | 218 | c3 := database.newConnection() 219 | c3.mustExecCommand("begin", nil) 220 | 221 | c1.mustExecCommand("set", []string{"x", "hey"}) 222 | c1.mustExecCommand("commit", nil) 223 | 224 | _, err := c2.execCommand("get", []string{"x"}) 225 | assertEq(err.Error(), "cannot get key that does not exist", "c5 get x") 226 | 227 | res, err := c2.execCommand("commit", nil) 228 | assertEq(res, "", "c2 commit") 229 | assertEq(err.Error(), "read-write conflict", "c2 commit") 230 | 231 | // But unrelated keys cause no conflict. 232 | c3.mustExecCommand("set", []string{"y", "no conflict"}) 233 | c3.mustExecCommand("commit", nil) 234 | } 235 | --------------------------------------------------------------------------------