go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/impl/cloud/datastore.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package cloud 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "strings" 22 "sync" 23 "time" 24 25 "cloud.google.com/go/datastore" 26 "google.golang.org/api/iterator" 27 pb "google.golang.org/genproto/googleapis/datastore/v1" 28 29 "go.chromium.org/luci/common/errors" 30 31 "go.chromium.org/luci/gae/impl/prod/constraints" 32 ds "go.chromium.org/luci/gae/service/datastore" 33 ) 34 35 type cloudDatastore struct { 36 client *datastore.Client 37 } 38 39 func (cds *cloudDatastore) use(c context.Context) context.Context { 40 return ds.SetRawFactory(c, func(ic context.Context) ds.RawInterface { 41 return &boundDatastore{ 42 Context: ic, 43 cloudDatastore: cds, 44 transaction: datastoreTransaction(ic), 45 kc: ds.GetKeyContext(ic), 46 } 47 }) 48 } 49 50 // boundDatastore is a bound instance of the cloudDatastore installed in the 51 // Context. 52 type boundDatastore struct { 53 context.Context 54 55 // Context is the bound user Context. It includes the datastore namespace, if 56 // one is set. 57 *cloudDatastore 58 59 transaction *transactionWrapper 60 kc ds.KeyContext 61 } 62 63 func (bds *boundDatastore) AllocateIDs(keys []*ds.Key, cb ds.NewKeyCB) error { 64 nativeKeys, err := bds.client.AllocateIDs(bds, gaeKeysToNative(keys)) 65 if err != nil { 66 return normalizeError(err) 67 } 68 for i, key := range nativeKeys { 69 cb(i, nativeKeyToGAE(bds.kc, key), nil) 70 } 71 return nil 72 } 73 74 func (bds *boundDatastore) RunInTransaction(fn func(context.Context) error, opts *ds.TransactionOptions) error { 75 if bds.transaction != nil { 76 return errors.New("nested transactions are not supported") 77 } 78 79 var txOpts []datastore.TransactionOption 80 if opts != nil { 81 if opts.ReadOnly { 82 txOpts = append(txOpts, datastore.ReadOnly) 83 } 84 if opts.Attempts > 0 { 85 txOpts = append(txOpts, datastore.MaxAttempts(opts.Attempts)) 86 } 87 } 88 89 _, err := bds.client.RunInTransaction(bds, func(tx *datastore.Transaction) error { 90 return fn(withDatastoreTransaction(bds, tx)) 91 }, txOpts...) 92 return normalizeError(err) 93 } 94 95 func (bds *boundDatastore) DecodeCursor(s string) (ds.Cursor, error) { 96 cursor, err := datastore.DecodeCursor(s) 97 return cursor, normalizeError(err) 98 } 99 100 func (bds *boundDatastore) Run(q *ds.FinalizedQuery, cb ds.RawRunCB) error { 101 it := bds.client.Run(bds, bds.prepareNativeQuery(q)) 102 cursorFn := func() (ds.Cursor, error) { 103 return it.Cursor() 104 } 105 106 for { 107 var npl *nativePropertyLoader 108 if !q.KeysOnly() { 109 npl = &nativePropertyLoader{kc: bds.kc} 110 } 111 nativeKey, err := it.Next(npl) 112 if err != nil { 113 if err == iterator.Done { 114 return nil 115 } 116 return normalizeError(err) 117 } 118 119 var pmap ds.PropertyMap 120 if npl != nil { 121 pmap = npl.pmap 122 } 123 if err := cb(nativeKeyToGAE(bds.kc, nativeKey), pmap, cursorFn); err != nil { 124 if err == ds.Stop { 125 return nil 126 } 127 return normalizeError(err) 128 } 129 } 130 } 131 132 func (bds *boundDatastore) Count(q *ds.FinalizedQuery) (int64, error) { 133 // If the query is eventually consistent, use faster server-side aggregation. 134 // For strongly-consistent queries we'll have to do local counting. 135 if q.EventuallyConsistent() { 136 res, err := bds.client.RunAggregationQuery(bds, 137 bds.prepareNativeQuery(q). 138 NewAggregationQuery(). 139 WithCount("total"), 140 ) 141 if err != nil { 142 return -1, normalizeError(err) 143 } 144 total, _ := res["total"].(*pb.Value) 145 if total == nil { 146 return -1, fmt.Errorf("aggregation result is unexpectedly missing") 147 } 148 return int64(total.GetIntegerValue()), nil 149 } 150 // Local counting. It is the only strongly-consistent method. 151 v, err := bds.client.Count(bds, bds.prepareNativeQuery(q)) 152 if err != nil { 153 return -1, normalizeError(err) 154 } 155 return int64(v), nil 156 } 157 158 func fixMultiError(err error) error { 159 if err == nil { 160 return nil 161 } 162 if baseME, ok := err.(datastore.MultiError); ok { 163 return errors.NewMultiError(baseME...) 164 } 165 return err 166 } 167 168 func idxCallbacker(err error, amt int, cb func(idx int, err error)) error { 169 if err == nil { 170 for i := 0; i < amt; i++ { 171 cb(i, nil) 172 } 173 return nil 174 } 175 176 err = fixMultiError(err) 177 if me, ok := err.(errors.MultiError); ok { 178 for i, err := range me { 179 cb(i, normalizeError(err)) 180 } 181 return nil 182 } 183 return normalizeError(err) 184 } 185 186 func (bds *boundDatastore) GetMulti(keys []*ds.Key, _meta ds.MultiMetaGetter, cb ds.GetMultiCB) error { 187 nativeKeys := gaeKeysToNative(keys) 188 nativePLS := make([]*nativePropertyLoader, len(nativeKeys)) 189 for i := range nativePLS { 190 nativePLS[i] = &nativePropertyLoader{kc: bds.kc} 191 } 192 193 var err error 194 if bds.transaction != nil { 195 // Transactional GetMulti. 196 err = bds.transaction.GetMulti(nativeKeys, nativePLS) 197 } else { 198 // Non-transactional GetMulti. 199 err = bds.client.GetMulti(bds, nativeKeys, nativePLS) 200 } 201 202 return idxCallbacker(err, len(nativePLS), func(idx int, err error) { 203 cb(idx, nativePLS[idx].pmap, err) 204 }) 205 } 206 207 func (bds *boundDatastore) PutMulti(keys []*ds.Key, vals []ds.PropertyMap, cb ds.NewKeyCB) error { 208 nativeKeys := gaeKeysToNative(keys) 209 nativePLS := make([]*nativePropertySaver, len(vals)) 210 for i := range nativePLS { 211 nativePLS[i] = &nativePropertySaver{kc: bds.kc, pmap: vals[i]} 212 } 213 214 var err error 215 if bds.transaction != nil { 216 // Transactional PutMulti. 217 // 218 // In order to simulate the presence of mid-transaction key allocation, we 219 // will identify any incomplete keys and allocate IDs for them. This is 220 // potentially wasteful in the event of failed or retried transactions, but 221 // it is required to maintain API compatibility with the datastore 222 // interface. 223 var incompleteKeys []*datastore.Key 224 var incompleteKeyMap map[int]int 225 for i, k := range nativeKeys { 226 if k.Incomplete() { 227 if incompleteKeyMap == nil { 228 // Optimization: if there are any incomplete keys, allocate room for 229 // the full range. 230 incompleteKeyMap = make(map[int]int, len(nativeKeys)-i) 231 incompleteKeys = make([]*datastore.Key, 0, len(nativeKeys)-i) 232 } 233 incompleteKeyMap[len(incompleteKeys)] = i 234 incompleteKeys = append(incompleteKeys, k) 235 } 236 } 237 if len(incompleteKeys) > 0 { 238 idKeys, err := bds.client.AllocateIDs(bds, incompleteKeys) 239 if err != nil { 240 return err 241 } 242 for i, idKey := range idKeys { 243 nativeKeys[incompleteKeyMap[i]] = idKey 244 } 245 } 246 247 _, err = bds.transaction.PutMulti(nativeKeys, nativePLS) 248 } else { 249 // Non-transactional PutMulti. 250 nativeKeys, err = bds.client.PutMulti(bds, nativeKeys, nativePLS) 251 } 252 253 return idxCallbacker(err, len(nativeKeys), func(idx int, err error) { 254 if err == nil { 255 cb(idx, nativeKeyToGAE(bds.kc, nativeKeys[idx]), nil) 256 return 257 } 258 cb(idx, nil, err) 259 }) 260 } 261 262 func (bds *boundDatastore) DeleteMulti(keys []*ds.Key, cb ds.DeleteMultiCB) error { 263 nativeKeys := gaeKeysToNative(keys) 264 265 var err error 266 if bds.transaction != nil { 267 // Transactional DeleteMulti. 268 err = bds.transaction.DeleteMulti(nativeKeys) 269 } else { 270 // Non-transactional DeleteMulti. 271 err = bds.client.DeleteMulti(bds, nativeKeys) 272 } 273 274 return idxCallbacker(err, len(nativeKeys), cb) 275 } 276 277 func (bds *boundDatastore) WithoutTransaction() context.Context { 278 return withoutDatastoreTransaction(bds) 279 } 280 281 func (bds *boundDatastore) CurrentTransaction() ds.Transaction { 282 if bds.transaction == nil { 283 return nil 284 } 285 return bds.transaction 286 } 287 288 func (bds *boundDatastore) Constraints() ds.Constraints { return constraints.DS() } 289 290 func (bds *boundDatastore) GetTestable() ds.Testable { return nil } 291 292 func (bds *boundDatastore) prepareNativeQuery(fq *ds.FinalizedQuery) *datastore.Query { 293 nq := datastore.NewQuery(fq.Kind()) 294 if bds.transaction != nil { 295 // NOTE: As of 2021 Q1 this is safe because it's documented that: 296 // 297 // "Queries are re-usable and it is safe to call Query.Run from concurrent 298 // goroutines" 299 // 300 // Inspecting the datastore client code reveals that it only uses the `id` 301 // field of the *Transaction object, not any of the state within the 302 // *Transaction object which needs protection via the *transactionWrapper. 303 nq = nq.Transaction(bds.transaction.tx) 304 } 305 if ns := bds.kc.Namespace; ns != "" { 306 nq = nq.Namespace(ns) 307 } 308 309 // nativeFilter translates a filter field. If the translation fails, we'll 310 // pass the result through to the underlying datastore and allow it to 311 // reject it. 312 nativeFilter := func(prop ds.Property) any { 313 if np, err := gaePropertyToNative(bds.kc, "", prop); err == nil { 314 return np.Value 315 } 316 return prop.Value() 317 } 318 319 // Equality filters. 320 for field, props := range fq.EqFilters() { 321 if field != "__ancestor__" { 322 for _, prop := range props { 323 nq = nq.FilterField(field, "=", nativeFilter(prop)) 324 } 325 } 326 } 327 for field, slices := range fq.InFilters() { 328 for _, slice := range slices { 329 native := make([]any, len(slice)) 330 for idx, prop := range slice { 331 native[idx] = nativeFilter(prop) 332 } 333 nq = nq.FilterField(field, "in", native) 334 } 335 } 336 337 // Inequality filters. 338 if ineq := fq.IneqFilterProp(); ineq != "" { 339 if field, op, prop := fq.IneqFilterLow(); field != "" { 340 nq = nq.FilterField(field, op, nativeFilter(prop)) 341 } 342 if field, op, prop := fq.IneqFilterHigh(); field != "" { 343 nq = nq.FilterField(field, op, nativeFilter(prop)) 344 } 345 } 346 347 start, end := fq.Bounds() 348 if start != nil { 349 nq = nq.Start(start.(datastore.Cursor)) 350 } 351 if end != nil { 352 nq = nq.End(end.(datastore.Cursor)) 353 } 354 355 if fq.Distinct() { 356 nq = nq.Distinct() 357 } 358 if fq.KeysOnly() { 359 nq = nq.KeysOnly() 360 } 361 if limit, ok := fq.Limit(); ok { 362 nq = nq.Limit(int(limit)) 363 } 364 if offset, ok := fq.Offset(); ok { 365 nq = nq.Offset(int(offset)) 366 } 367 if proj := fq.Project(); proj != nil { 368 nq = nq.Project(proj...) 369 } 370 if ancestor := fq.Ancestor(); ancestor != nil { 371 nq = nq.Ancestor(gaeKeyToNative(ancestor)) 372 } 373 if fq.EventuallyConsistent() { 374 nq = nq.EventualConsistency() 375 } 376 377 for _, ic := range fq.Orders() { 378 prop := ic.Property 379 if ic.Descending { 380 prop = "-" + prop 381 } 382 nq = nq.Order(prop) 383 } 384 385 return nq 386 } 387 388 func gaePropertyToNative(kc ds.KeyContext, name string, pdata ds.PropertyData) (nativeProp datastore.Property, err error) { 389 nativeProp.Name = name 390 391 convert := func(prop *ds.Property) (any, error) { 392 switch pt := prop.Type(); pt { 393 case ds.PTNull, ds.PTInt, ds.PTTime, ds.PTBool, ds.PTBytes, ds.PTString, ds.PTFloat: 394 return prop.Value(), nil 395 396 case ds.PTGeoPoint: 397 gp := prop.Value().(ds.GeoPoint) 398 return datastore.GeoPoint{Lat: gp.Lat, Lng: gp.Lng}, nil 399 400 case ds.PTKey: 401 return gaeKeyToNative(prop.Value().(*ds.Key)), nil 402 403 case ds.PTPropertyMap: 404 return gaeEntityToNative(kc, prop.Value().(ds.PropertyMap)), nil 405 406 default: 407 return nil, fmt.Errorf("unsupported property type: %v", pt) 408 } 409 } 410 411 switch t := pdata.(type) { 412 case ds.Property: 413 if nativeProp.Value, err = convert(&t); err != nil { 414 return 415 } 416 nativeProp.NoIndex = (t.IndexSetting() != ds.ShouldIndex) 417 418 case ds.PropertySlice: 419 // Don't index by default. If *any* sub-property requests being indexed, 420 // then we will index. 421 nativeProp.NoIndex = true 422 423 // Pack this into an any so it is marked as a multi-value. 424 multiProp := make([]any, len(t)) 425 for i := range t { 426 prop := &t[i] 427 if multiProp[i], err = convert(prop); err != nil { 428 return 429 } 430 431 if prop.IndexSetting() == ds.ShouldIndex { 432 nativeProp.NoIndex = false 433 } 434 } 435 nativeProp.Value = multiProp 436 437 default: 438 err = fmt.Errorf("unsupported PropertyData type for %q: %T", name, pdata) 439 } 440 441 return 442 } 443 444 func nativePropertyToGAE(kc ds.KeyContext, nativeProp datastore.Property) (name string, pdata ds.PropertyData, err error) { 445 name = nativeProp.Name 446 447 convert := func(nv any, prop *ds.Property) error { 448 switch nvt := nv.(type) { 449 case nil: 450 nv = nil 451 452 case int64, bool, string, float64: 453 break 454 455 case []byte: 456 if len(nvt) == 0 { 457 // Cloud datastore library returns []byte{} if it is empty. 458 // Make it nil as more convenient to deal with in tests. 459 nv = []byte(nil) 460 } 461 462 case time.Time: 463 // Cloud datastore library returns local time. 464 nv = nvt.UTC() 465 466 case datastore.GeoPoint: 467 nv = ds.GeoPoint{Lat: nvt.Lat, Lng: nvt.Lng} 468 469 case *datastore.Key: 470 nv = nativeKeyToGAE(kc, nvt) 471 472 case *datastore.Entity: 473 nv = nativeEntityToGAE(kc, nvt) 474 475 default: 476 return fmt.Errorf("unsupported datastore.Value type for %q: %T", name, nvt) 477 } 478 479 indexSetting := ds.ShouldIndex 480 if nativeProp.NoIndex { 481 indexSetting = ds.NoIndex 482 } 483 prop.SetValue(nv, indexSetting) 484 return nil 485 } 486 487 // Slice of supported native type. Convert this into PropertySlice. 488 // 489 // It must be an []any. 490 if nativeValues, ok := nativeProp.Value.([]any); ok { 491 pslice := make(ds.PropertySlice, len(nativeValues)) 492 for i, nv := range nativeValues { 493 if err = convert(nv, &pslice[i]); err != nil { 494 return 495 } 496 } 497 pdata = pslice 498 return 499 } 500 501 var prop ds.Property 502 if err = convert(nativeProp.Value, &prop); err != nil { 503 return 504 } 505 pdata = prop 506 return 507 } 508 509 func gaeKeyToNative(key *ds.Key) *datastore.Key { 510 var nativeKey *datastore.Key 511 512 _, _, toks := key.Split() 513 for _, tok := range toks { 514 nativeKey = &datastore.Key{ 515 Kind: tok.Kind, 516 ID: tok.IntID, 517 Name: tok.StringID, 518 Parent: nativeKey, 519 Namespace: key.Namespace(), 520 } 521 } 522 523 return nativeKey 524 } 525 526 func gaeKeysToNative(keys []*ds.Key) []*datastore.Key { 527 nativeKeys := make([]*datastore.Key, len(keys)) 528 for i, key := range keys { 529 nativeKeys[i] = gaeKeyToNative(key) 530 } 531 return nativeKeys 532 } 533 534 func nativeKeyToGAE(kc ds.KeyContext, nativeKey *datastore.Key) *ds.Key { 535 toks := make([]ds.KeyTok, 0, 2) 536 537 cur := nativeKey 538 for { 539 toks = append(toks, ds.KeyTok{Kind: cur.Kind, IntID: cur.ID, StringID: cur.Name}) 540 cur = cur.Parent 541 if cur == nil { 542 break 543 } 544 } 545 546 // Reverse "toks" so we have ancestor-to-child lineage. 547 for i := 0; i < len(toks)/2; i++ { 548 ri := len(toks) - i - 1 549 toks[i], toks[ri] = toks[ri], toks[i] 550 } 551 552 kc.Namespace = nativeKey.Namespace 553 return kc.NewKeyToks(toks) 554 } 555 556 // nativeEntityToGAE returns a ds.PropertyMap representation of the given 557 // *datastore.Entity. Since properties can themselves be *datastore.Entities, 558 // the caller is responsible for ensuring there are no reference cycles. 559 func nativeEntityToGAE(kc ds.KeyContext, ent *datastore.Entity) ds.PropertyMap { 560 if ent == nil { 561 return nil 562 } 563 pm := make(ds.PropertyMap, len(ent.Properties)+4) 564 if ent.Key != nil { 565 // Populate all potentially supported meta properties. Whatever consumes 566 // the property map (usually the default struct PLS) will choose properties 567 // it cares about and ignore the rest. 568 ds.PopulateKey(pm, nativeKeyToGAE(kc, ent.Key)) 569 } 570 // Property ordering is lost since it's encoded to a map, but *datastore.Entity is 571 // sourced from https://godoc.org/google.golang.org/genproto/googleapis/datastore/v1#Entity 572 // which originally held properties in a map to begin with, meaning order is irrelevant. 573 for _, p := range ent.Properties { 574 _, prop, err := nativePropertyToGAE(kc, p) 575 if err != nil { 576 // Shouldn't happen. It means the *datastore.Entity contained an unsupported type. 577 panic(err) 578 } 579 pm[p.Name] = prop 580 } 581 return pm 582 } 583 584 // gaeEntityToNative returns a *datastore.Entity representation of the given 585 // PropertyMap (assumed to have been produced by nativeEntityToGAE). 586 func gaeEntityToNative(kc ds.KeyContext, pm ds.PropertyMap) *datastore.Entity { 587 // Ensure stable order. Skip meta fields, they'll be used in NewKeyFromMeta. 588 keys := make([]string, 0, len(pm)) 589 for name := range pm { 590 if !strings.HasPrefix(name, "$") { 591 keys = append(keys, name) 592 } 593 } 594 sort.Strings(keys) 595 596 ent := &datastore.Entity{ 597 Properties: make([]datastore.Property, 0, len(keys)), 598 } 599 600 // Try to extract the entity key from available meta fields. Ignore incomplete 601 // keys. This actually happens for structs that don't have any explicitly 602 // defined meta properties (because `$kind` is implicitly defined, so they end 603 // up with an incomplete key, since they have no `$id`). 604 if key, _ := kc.NewKeyFromMeta(pm); key != nil && !key.IsIncomplete() { 605 ent.Key = gaeKeyToNative(key) 606 } 607 608 // Convert non-meta fields. 609 for _, name := range keys { 610 p, err := gaePropertyToNative(kc, name, pm[name]) 611 if err != nil { 612 // Shouldn't happen. It means nativeEntityToGAE encoded an unsupported type. 613 panic(err) 614 } 615 ent.Properties = append(ent.Properties, p) 616 } 617 return ent 618 } 619 620 // nativePropertyLoader is a datastore.PropertyLoadSaver that implement Load 621 // by writing properties into a ds.PropertyMap. 622 type nativePropertyLoader struct { 623 kc ds.KeyContext 624 pmap ds.PropertyMap // starts as nil, gets created and populated in Load 625 } 626 627 var _ datastore.PropertyLoadSaver = (*nativePropertyLoader)(nil) 628 629 func (npl *nativePropertyLoader) Load(props []datastore.Property) error { 630 if npl.pmap == nil { 631 npl.pmap = make(ds.PropertyMap, len(props)) 632 } 633 634 for _, nativeProp := range props { 635 name, pdata, err := nativePropertyToGAE(npl.kc, nativeProp) 636 if err != nil { 637 return err 638 } 639 if _, ok := npl.pmap[name]; ok { 640 return fmt.Errorf("duplicate properties for %q", name) 641 } 642 npl.pmap[name] = pdata 643 } 644 return nil 645 } 646 647 func (npl *nativePropertyLoader) Save() ([]datastore.Property, error) { 648 panic("must not be called") 649 } 650 651 // nativePropertySaver is a datastore.PropertyLoadSaver that implement Save 652 // by reading properties from a ds.PropertyMap. 653 type nativePropertySaver struct { 654 kc ds.KeyContext 655 pmap ds.PropertyMap // must be set by the caller 656 } 657 658 var _ datastore.PropertyLoadSaver = (*nativePropertySaver)(nil) 659 660 func (nps *nativePropertySaver) Load(props []datastore.Property) error { 661 panic("must not be called") 662 } 663 664 func (nps *nativePropertySaver) Save() ([]datastore.Property, error) { 665 if len(nps.pmap) == 0 { 666 return nil, nil 667 } 668 669 props := make([]datastore.Property, 0, len(nps.pmap)) 670 for name, pdata := range nps.pmap { 671 // Strip meta. 672 if strings.HasPrefix(name, "$") { 673 continue 674 } 675 676 nativeProp, err := gaePropertyToNative(nps.kc, name, pdata) 677 if err != nil { 678 return nil, err 679 } 680 props = append(props, nativeProp) 681 } 682 return props, nil 683 } 684 685 // transactionWrapper provides a Mutex around mutation calls on the Transaction. 686 // 687 // This is required until https://github.com/googleapis/google-cloud-go/issues/3750 688 // is fixed. 689 type transactionWrapper struct { 690 mu sync.Mutex 691 tx *datastore.Transaction 692 } 693 694 func (tw *transactionWrapper) GetMulti(keys []*datastore.Key, dst any) (err error) { 695 // We don't acquire a lock here because as of 2021 Q1 Transaction.GetMulti 696 // only reads the Transaction.id field, and doesn't make any mutations to the 697 // *Transaction state at all. 698 return tw.tx.GetMulti(keys, dst) 699 } 700 701 func (tw *transactionWrapper) PutMulti(keys []*datastore.Key, src any) (ret []*datastore.PendingKey, err error) { 702 tw.mu.Lock() 703 defer tw.mu.Unlock() 704 return tw.tx.PutMulti(keys, src) 705 } 706 707 func (tw *transactionWrapper) DeleteMulti(keys []*datastore.Key) (err error) { 708 tw.mu.Lock() 709 defer tw.mu.Unlock() 710 return tw.tx.DeleteMulti(keys) 711 } 712 713 var datastoreTransactionKey = "*transactionWrapper" 714 715 func withDatastoreTransaction(c context.Context, tx *datastore.Transaction) context.Context { 716 return context.WithValue(c, &datastoreTransactionKey, &transactionWrapper{tx: tx}) 717 } 718 719 func withoutDatastoreTransaction(c context.Context) context.Context { 720 return context.WithValue(c, &datastoreTransactionKey, nil) 721 } 722 723 func datastoreTransaction(c context.Context) *transactionWrapper { 724 if tw, ok := c.Value(&datastoreTransactionKey).(*transactionWrapper); ok { 725 return tw 726 } 727 return nil 728 } 729 730 func normalizeError(err error) error { 731 switch err { 732 case datastore.ErrNoSuchEntity: 733 return ds.ErrNoSuchEntity 734 case datastore.ErrConcurrentTransaction: 735 return ds.ErrConcurrentTransaction 736 case datastore.ErrInvalidKey: 737 return ds.MakeErrInvalidKey("").Err() 738 default: 739 return err 740 } 741 }