github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/connectors/memory/memory.go (about) 1 // Copyright (c) 2017 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package memory 22 23 import ( 24 "bytes" 25 "context" 26 "encoding/gob" 27 "sort" 28 "sync" 29 "time" 30 31 "encoding/binary" 32 33 "encoding/base64" 34 35 "github.com/pkg/errors" 36 "github.com/satori/go.uuid" 37 "github.com/uber-go/dosa" 38 "github.com/uber-go/dosa/connectors/base" 39 ) 40 41 // Connector is an in-memory connector. 42 // The in-memory connector stores its data like this: 43 // map[string]map[string][]map[string]dosa.FieldValue 44 // 45 // the first 'string' is the table name (entity name) 46 // the second 'string' is the partition key, encoded using encoding/gob to guarantee uniqueness 47 // within each 'partition' you have a list of rows ([]map[string]dosa.FieldValue) 48 // these rows are kept ordered so that reads are lightning fast and searches are quick too 49 // the row itself is a map of field name to value (map[string]dosaFieldValue]) 50 // 51 // A read-write mutex lock is used to control concurrency, making reads work in parallel but 52 // writes are not. There is no attempt to improve the concurrency of the read or write path by 53 // adding more granular locks. 54 type Connector struct { 55 base.Connector 56 data map[string]map[string][]map[string]dosa.FieldValue 57 lock sync.RWMutex 58 } 59 60 // partitionRange represents one section of a partition. 61 type partitionRange struct { 62 entityRef map[string][]map[string]dosa.FieldValue 63 partitionKey string 64 start int 65 end int 66 } 67 68 // remove deletes the values referenced by the partitionRange. Since this function modifies 69 // the data stored in the in-memory connector, a write lock must be held when calling 70 // this function. 71 // 72 // Note this function can't be called more than once. Calling it more than once will cause a panic. 73 func (pr *partitionRange) remove() { 74 partitionRef := pr.entityRef[pr.partitionKey] 75 pr.entityRef[pr.partitionKey] = append(partitionRef[:pr.start], partitionRef[pr.end+1:]...) 76 pr.entityRef = nil 77 pr.partitionKey = "" 78 pr.start = 0 79 pr.end = 0 80 } 81 82 // values returns all the values in the partition range 83 func (pr *partitionRange) values() []map[string]dosa.FieldValue { 84 return pr.entityRef[pr.partitionKey][pr.start : pr.end+1] 85 } 86 87 // partitionKeyBuilder extracts the partition key components from the map and encodes them, 88 // generating a unique string. It uses the encoding/gob method to make a byte array as the 89 // key, and returns this as a string 90 func partitionKeyBuilder(pk *dosa.PrimaryKey, values map[string]dosa.FieldValue) (string, error) { 91 encodedKey := bytes.Buffer{} 92 encoder := gob.NewEncoder(&encodedKey) 93 for _, k := range pk.PartitionKeys { 94 if v, ok := values[k]; ok { 95 _ = encoder.Encode(v) 96 } else { 97 return "", errors.Errorf("Missing value for partition key %q", k) 98 } 99 } 100 return string(encodedKey.Bytes()), nil 101 } 102 103 // findInsertionPoint locates the place within a partition where the data belongs. 104 // It inspects the clustering key values found in the insertMe value and figures out 105 // where they go in the data slice. It doesn't change anything, but it does let you 106 // know if it found an exact match or if it's just not there. When it's not there, 107 // it indicates where it is supposed to get inserted 108 func findInsertionPoint(pk *dosa.PrimaryKey, data []map[string]dosa.FieldValue, insertMe map[string]dosa.FieldValue) (found bool, idx int) { 109 found = false 110 idx = sort.Search(len(data), func(offset int) bool { 111 cmp := compareRows(pk, data[offset], insertMe) 112 if cmp == 0 { 113 found = true 114 } 115 return cmp >= 0 116 }) 117 return 118 } 119 120 // compareRows compares two maps of row data based on clustering keys. It handles ascending/descending 121 // based on the passed-in schema 122 func compareRows(pk *dosa.PrimaryKey, v1 map[string]dosa.FieldValue, v2 map[string]dosa.FieldValue) (cmp int8) { 123 keys := pk.ClusteringKeys 124 for _, key := range keys { 125 d1 := v1[key.Name] 126 d2 := v2[key.Name] 127 cmp = compareType(d1, d2) 128 if key.Descending { 129 cmp = -cmp 130 } 131 if cmp != 0 { 132 return cmp 133 } 134 } 135 return cmp 136 } 137 138 // This function returns the time bits from a UUID 139 // You would have to scale this to nanos to make a 140 // time.Time but we don't generally need that for 141 // comparisons. See RFC 4122 for these bit offsets 142 func timeFromUUID(u uuid.UUID) int64 { 143 low := int64(binary.BigEndian.Uint32(u[0:4])) 144 mid := int64(binary.BigEndian.Uint16(u[4:6])) 145 hi := int64((binary.BigEndian.Uint16(u[6:8]) & 0x0fff)) 146 return low + (mid << 32) + (hi << 48) 147 } 148 149 // compareType compares a single DOSA field based on the type. This code assumes the types of each 150 // of the columns are the same, or it will panic 151 func compareType(d1 dosa.FieldValue, d2 dosa.FieldValue) int8 { 152 switch d1 := d1.(type) { 153 case dosa.UUID: 154 u1 := uuid.FromStringOrNil(string(d1)) 155 u2 := uuid.FromStringOrNil(string(d2.(dosa.UUID))) 156 if u1.Version() != u2.Version() { 157 if u1.Version() < u2.Version() { 158 return -1 159 } 160 return 1 161 } 162 if u1.Version() == 1 { 163 // compare time UUIDs 164 t1 := timeFromUUID(u1) 165 t2 := timeFromUUID(u2) 166 if t1 == t2 { 167 return 0 168 } 169 if t1 < t2 { 170 return -1 171 } 172 return 1 173 } 174 175 // version 176 if string(d1) == string(d2.(dosa.UUID)) { 177 return 0 178 } 179 if string(d1) < string(d2.(dosa.UUID)) { 180 return -1 181 } 182 return 1 183 case string: 184 if d1 == d2.(string) { 185 return 0 186 } 187 if d1 < d2.(string) { 188 return -1 189 } 190 return 1 191 case int64: 192 if d1 == d2.(int64) { 193 return 0 194 } 195 if d1 < d2.(int64) { 196 return -1 197 } 198 return 1 199 case int32: 200 if d1 == d2.(int32) { 201 return 0 202 } 203 if d1 < d2.(int32) { 204 return -1 205 } 206 return 1 207 case float64: 208 if d1 == d2.(float64) { 209 return 0 210 } 211 if d1 < d2.(float64) { 212 return -1 213 } 214 return 1 215 case []byte: 216 c := bytes.Compare(d1, d2.([]byte)) 217 if c == 0 { 218 return 0 219 } 220 if c < 0 { 221 return -1 222 } 223 return 1 224 case time.Time: 225 if d1.Equal(d2.(time.Time)) { 226 return 0 227 } 228 if d1.Before(d2.(time.Time)) { 229 return -1 230 } 231 return 1 232 case bool: 233 if d1 == d2.(bool) { 234 return 0 235 } 236 if d1 == false { 237 return -1 238 } 239 return 1 240 } 241 panic(d1) 242 } 243 244 // CreateIfNotExists inserts a row if it isn't already there. The basic flow is: 245 // Find the partition, if it's not there, then create it and insert the row there 246 // If the partition is there, and there's data in it, and there's no clustering key, then fail 247 // Otherwise, search the partition for the exact same clustering keys. If there, fail 248 // if not, then insert it at the right spot (sort.Search does most of the heavy lifting here) 249 func (c *Connector) CreateIfNotExists(_ context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue) error { 250 c.lock.Lock() 251 defer c.lock.Unlock() 252 err := c.mergedInsert(ei.Def.Name, ei.Def.Key, values, func(into map[string]dosa.FieldValue, from map[string]dosa.FieldValue) error { 253 return &dosa.ErrAlreadyExists{} 254 }) 255 if err != nil { 256 return err 257 } 258 for iName, iDef := range ei.Def.Indexes { 259 // this error must be ignored, so we skip indexes when the value 260 // for one of the index fields is not specified 261 _ = c.mergedInsert(iName, ei.Def.UniqueKey(iDef.Key), values, overwriteValuesFunc) 262 } 263 return nil 264 } 265 266 // Read searches for a row. First, it finds the partition, then it searches in the partition for 267 // the data, and returns it when it finds it. Again, sort.Search does most of the heavy lifting 268 // within a partition 269 func (c *Connector) Read(_ context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue, minimumFields []string) (map[string]dosa.FieldValue, error) { 270 c.lock.RLock() 271 defer c.lock.RUnlock() 272 entityRef := c.data[ei.Def.Name] 273 encodedPartitionKey, err := partitionKeyBuilder(ei.Def.Key, values) 274 if err != nil { 275 return nil, errors.Wrapf(err, "Cannot build partition key for entity %q", ei.Def.Name) 276 } 277 if c.data[ei.Def.Name] == nil { 278 return nil, &dosa.ErrNotFound{} 279 } 280 partitionRef := entityRef[encodedPartitionKey] 281 // no data in this partition? easy out! 282 if len(partitionRef) == 0 { 283 return nil, &dosa.ErrNotFound{} 284 } 285 286 if len(ei.Def.Key.ClusteringKeySet()) == 0 { 287 return partitionRef[0], nil 288 } 289 // clustering key, search for the value in the set 290 found, inx := findInsertionPoint(ei.Def.Key, partitionRef, values) 291 if !found { 292 return nil, &dosa.ErrNotFound{} 293 } 294 return partitionRef[inx], nil 295 } 296 297 func overwriteValuesFunc(into map[string]dosa.FieldValue, from map[string]dosa.FieldValue) error { 298 for k, v := range from { 299 into[k] = v 300 } 301 return nil 302 } 303 304 // Upsert works a lot like CreateIfNotExists but merges the data when it finds an existing row 305 func (c *Connector) Upsert(_ context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue) error { 306 c.lock.Lock() 307 defer c.lock.Unlock() 308 if err := c.mergedInsert(ei.Def.Name, ei.Def.Key, values, overwriteValuesFunc); err != nil { 309 return err 310 } 311 for iName, iDef := range ei.Def.Indexes { 312 _ = c.mergedInsert(iName, ei.Def.UniqueKey(iDef.Key), values, overwriteValuesFunc) 313 } 314 315 return nil 316 } 317 318 func (c *Connector) mergedInsert(name string, 319 pk *dosa.PrimaryKey, 320 values map[string]dosa.FieldValue, 321 mergeFunc func(map[string]dosa.FieldValue, map[string]dosa.FieldValue) error) error { 322 323 if c.data[name] == nil { 324 c.data[name] = make(map[string][]map[string]dosa.FieldValue) 325 } 326 entityRef := c.data[name] 327 encodedPartitionKey, err := partitionKeyBuilder(pk, values) 328 if err != nil { 329 return errors.Wrapf(err, "Cannot build partition key for %q", name) 330 } 331 if entityRef[encodedPartitionKey] == nil { 332 entityRef[encodedPartitionKey] = make([]map[string]dosa.FieldValue, 0, 1) 333 } 334 partitionRef := entityRef[encodedPartitionKey] 335 // no data in this partition? easy out! 336 if len(partitionRef) == 0 { 337 entityRef[encodedPartitionKey] = append(entityRef[encodedPartitionKey], values) 338 return nil 339 } 340 341 if len(pk.ClusteringKeySet()) == 0 { 342 // no clustering key, so the row must already exist, merge it 343 return mergeFunc(partitionRef[0], values) 344 } 345 // there is a clustering key, find the insertion point (binary search would be fastest) 346 found, offset := findInsertionPoint(pk, partitionRef, values) 347 if found { 348 return mergeFunc(partitionRef[offset], values) 349 } 350 // perform slice magic to insert value at given offset 351 l := len(entityRef[encodedPartitionKey]) // get length 352 entityRef[encodedPartitionKey] = append(entityRef[encodedPartitionKey], entityRef[encodedPartitionKey][l-1]) // copy last element 353 // scoot over remaining elements 354 copy(entityRef[encodedPartitionKey][offset+1:], entityRef[encodedPartitionKey][offset:]) 355 // and plunk value into appropriate location 356 entityRef[encodedPartitionKey][offset] = values 357 return nil 358 } 359 360 // Remove deletes a single row 361 // There's no way to return an error from this method 362 func (c *Connector) Remove(_ context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue) error { 363 c.lock.Lock() 364 defer c.lock.Unlock() 365 if c.data[ei.Def.Name] == nil { 366 return nil 367 } 368 for iName, iDef := range ei.Def.Indexes { 369 c.removeItem(iName, ei.Def.UniqueKey(iDef.Key), values) 370 } 371 c.removeItem(ei.Def.Name, ei.Def.Key, values) 372 return nil 373 } 374 375 func (c *Connector) removeItem(name string, key *dosa.PrimaryKey, values map[string]dosa.FieldValue) { 376 entityRef := c.data[name] 377 encodedPartitionKey, err := partitionKeyBuilder(key, values) 378 if err != nil || entityRef[encodedPartitionKey] == nil { 379 return 380 } 381 partitionRef := entityRef[encodedPartitionKey] 382 // no data in this partition? easy out! 383 if len(partitionRef) == 0 { 384 return 385 } 386 387 // no clustering keys? Simple, delete this 388 if len(key.ClusteringKeySet()) == 0 { 389 // NOT delete(entityRef, encodedPartitionKey) 390 // Unfortunately, Scan relies on the fact that these are not completely deleted 391 entityRef[encodedPartitionKey] = nil 392 return 393 } 394 found, offset := findInsertionPoint(key, partitionRef, values) 395 if found { 396 entityRef[encodedPartitionKey] = append(entityRef[encodedPartitionKey][:offset], entityRef[encodedPartitionKey][offset+1:]...) 397 } 398 return 399 } 400 401 // RemoveRange removes all of the elements in the range specified by the entity info and the column conditions. 402 func (c *Connector) RemoveRange(_ context.Context, ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition) error { 403 c.lock.Lock() 404 defer c.lock.Unlock() 405 406 partitionRange, err := c.findRange(ei, columnConditions) 407 if err != nil { 408 return err 409 } 410 // TODO: this should have been from a primary key lookup, not an index lookup 411 if partitionRange != nil { 412 for iName, iDef := range ei.Def.Indexes { 413 for _, vals := range partitionRange.values() { 414 c.removeItem(iName, ei.Def.UniqueKey(iDef.Key), vals) 415 } 416 } 417 partitionRange.remove() 418 } 419 420 return nil 421 } 422 423 // Range returns a slice of data from the datastore 424 func (c *Connector) Range(_ context.Context, ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition, minimumFields []string, token string, limit int) ([]map[string]dosa.FieldValue, string, error) { 425 c.lock.RLock() 426 defer c.lock.RUnlock() 427 428 partitionRange, err := c.findRange(ei, columnConditions) 429 if err != nil { 430 return nil, "", errors.Wrap(err, "Invalid range conditions") 431 } 432 if partitionRange == nil { 433 return []map[string]dosa.FieldValue{}, "", nil 434 } 435 436 if token != "" { 437 // if we have a token, use it to determine the offset to start from 438 values, err := decodeToken(token) 439 if err != nil { 440 return nil, "", errors.Wrapf(err, "Invalid token %q", token) 441 } 442 found, offset := findInsertionPoint(ei.Def.Key, partitionRange.values(), values) 443 if found { 444 partitionRange.start += offset + 1 445 } else { 446 partitionRange.start += offset 447 } 448 } 449 slice := partitionRange.values() 450 token = "" 451 if len(slice) > limit { 452 token = makeToken(slice[limit-1]) 453 slice = slice[:limit] 454 } 455 return slice, token, nil 456 } 457 458 func makeToken(v map[string]dosa.FieldValue) string { 459 encodedKey := bytes.Buffer{} 460 encoder := gob.NewEncoder(&encodedKey) 461 gob.Register(dosa.UUID("")) 462 err := encoder.Encode(v) 463 if err != nil { 464 // this should really be impossible, unless someone forgot to 465 // register some newly supported type with the encoder 466 panic(err) 467 } 468 return base64.StdEncoding.EncodeToString([]byte(encodedKey.String())) 469 } 470 471 func decodeToken(token string) (values map[string]dosa.FieldValue, err error) { 472 gobData, err := base64.StdEncoding.DecodeString(token) 473 if err != nil { 474 return nil, err 475 } 476 gobReader := bytes.NewBuffer(gobData) 477 gob.Register(dosa.UUID("")) 478 decoder := gob.NewDecoder(gobReader) 479 err = decoder.Decode(&values) 480 return values, err 481 } 482 483 // findRange finds the partitionRange specified by the given entity info and column conditions. 484 // In the case that no entities are found an empty partitionRange with a nil partition field will be returned. 485 // 486 // Note that this function reads from the connector's data map. Any calling functions should hold 487 // at least a read lock on the map. 488 func (c *Connector) findRange(ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition) (*partitionRange, error) { 489 // no data at all, fine 490 if c.data[ei.Def.Name] == nil { 491 return nil, nil 492 } 493 494 // find the equals conditions on each of the partition keys 495 values := make(map[string]dosa.FieldValue) 496 497 // figure out which "table" or "index" to use based on the supplied conditions 498 name, key, err := ei.IndexFromConditions(columnConditions) 499 if err != nil { 500 return nil, err 501 } 502 503 for _, pk := range key.PartitionKeys { 504 values[pk] = columnConditions[pk][0].Value 505 } 506 507 entityRef := c.data[name] 508 // an error is impossible here, since the partition keys must be set from IndexFromConditions 509 encodedPartitionKey, _ := partitionKeyBuilder(key, values) 510 partitionRef := entityRef[encodedPartitionKey] 511 // no data in this partition? easy out! 512 if len(partitionRef) == 0 { 513 return nil, nil 514 } 515 // hunt through the partitionRef and return values that match search criteria 516 // TODO: This can be done much faster using a binary search 517 startinx, endinx := 0, len(partitionRef)-1 518 for startinx < len(partitionRef) && !matchesClusteringConditions(ei, columnConditions, partitionRef[startinx]) { 519 startinx++ 520 } 521 for endinx >= startinx && !matchesClusteringConditions(ei, columnConditions, partitionRef[endinx]) { 522 endinx-- 523 524 } 525 if endinx < startinx { 526 return nil, nil 527 } 528 529 return &partitionRange{ 530 entityRef: entityRef, 531 partitionKey: encodedPartitionKey, 532 start: startinx, 533 end: endinx, 534 }, nil 535 } 536 537 // matchesClusteringConditions checks if a data row matches the conditions in the columnConditions that apply to 538 // clustering columns. If a condition does NOT match, it returns false, otherwise true 539 // This function is pretty fast if there are no conditions on the clustering columns 540 func matchesClusteringConditions(ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition, data map[string]dosa.FieldValue) bool { 541 for _, col := range ei.Def.Key.ClusteringKeys { 542 if conds, ok := columnConditions[col.Name]; ok { 543 // conditions exist on this clustering key 544 for _, cond := range conds { 545 if !passCol(data[col.Name], cond) { 546 return false 547 } 548 } 549 } 550 } 551 return true 552 } 553 554 // passCol checks if a column passes a specific condition 555 func passCol(data dosa.FieldValue, cond *dosa.Condition) bool { 556 cmp := compareType(data, cond.Value) 557 switch cond.Op { 558 case dosa.Eq: 559 return cmp == 0 560 case dosa.Gt: 561 return cmp > 0 562 case dosa.GtOrEq: 563 return cmp >= 0 564 case dosa.Lt: 565 return cmp < 0 566 case dosa.LtOrEq: 567 return cmp <= 0 568 } 569 panic("invalid operator " + cond.Op.String()) 570 } 571 572 // Scan returns all the rows 573 func (c *Connector) Scan(_ context.Context, ei *dosa.EntityInfo, minimumFields []string, token string, limit int) ([]map[string]dosa.FieldValue, string, error) { 574 c.lock.RLock() 575 defer c.lock.RUnlock() 576 if c.data[ei.Def.Name] == nil { 577 return []map[string]dosa.FieldValue{}, "", nil 578 579 } 580 entityRef := c.data[ei.Def.Name] 581 allTheThings := make([]map[string]dosa.FieldValue, 0) 582 583 // in order for Scan to be deterministic and continuable, we have 584 // to sort the primary key references 585 keys := make([]string, 0, len(entityRef)) 586 for key := range entityRef { 587 keys = append(keys, key) 588 } 589 sort.Strings(keys) 590 591 // if there was a token, decode it so we can determine the starting 592 // partition key 593 startPartKey, start, err := getStartingPoint(ei, token) 594 if err != nil { 595 return nil, "", errors.Wrapf(err, "Invalid token %s", token) 596 } 597 598 for _, key := range keys { 599 if startPartKey != "" { 600 // had a token, so we need to either partially skip or fully skip 601 // depending on whether we found the token's partition key yet 602 if key == startPartKey { 603 // we reached the starting partition key, so stop skipping 604 // future values, and add a portion of this one to the set 605 startPartKey = "" 606 found, offset := findInsertionPoint(ei.Def.Key, entityRef[key], start) 607 if found { 608 offset++ 609 } 610 allTheThings = append(allTheThings, entityRef[key][offset:]...) 611 } // else keep looking for this partition key 612 continue 613 } 614 allTheThings = append(allTheThings, entityRef[key]...) 615 } 616 if len(allTheThings) == 0 { 617 return []map[string]dosa.FieldValue{}, "", nil 618 } 619 // see if we need a token to return 620 token = "" 621 if len(allTheThings) > limit { 622 token = makeToken(allTheThings[limit-1]) 623 allTheThings = allTheThings[:limit] 624 } 625 return allTheThings, token, nil 626 } 627 628 // getStartingPoint determines the partition key of the starting point to resume a scan 629 // when a token is provided 630 func getStartingPoint(ei *dosa.EntityInfo, token string) (start string, startPartKey map[string]dosa.FieldValue, err error) { 631 if token == "" { 632 return "", map[string]dosa.FieldValue{}, nil 633 } 634 startPartKey, err = decodeToken(token) 635 if err != nil { 636 return "", map[string]dosa.FieldValue{}, errors.Wrapf(err, "Invalid token %q", token) 637 } 638 start, err = partitionKeyBuilder(ei.Def.Key, startPartKey) 639 if err != nil { 640 return "", map[string]dosa.FieldValue{}, errors.Wrapf(err, "Can't build partition key for %q", ei.Def.Name) 641 } 642 return start, startPartKey, nil 643 } 644 645 // CheckSchema is just a stub; there is no schema management for the in memory connector 646 // since creating a new one leaves you with no data! 647 func (c *Connector) CheckSchema(ctx context.Context, scope, namePrefix string, ed []*dosa.EntityDefinition) (int32, error) { 648 return 1, nil 649 } 650 651 // Shutdown deletes all the data 652 func (c *Connector) Shutdown() error { 653 c.lock.Lock() 654 defer c.lock.Unlock() 655 c.data = nil 656 return nil 657 } 658 659 // NewConnector creates a new in-memory connector 660 func NewConnector() *Connector { 661 c := Connector{} 662 c.data = make(map[string]map[string][]map[string]dosa.FieldValue) 663 return &c 664 } 665 666 func init() { 667 dosa.RegisterConnector("memory", func(dosa.CreationArgs) (dosa.Connector, error) { 668 return NewConnector(), nil 669 }) 670 }