go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/model/taskid.go (about) 1 // Copyright 2023 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 model 16 17 import ( 18 "context" 19 "fmt" 20 "strconv" 21 "time" 22 23 "go.chromium.org/luci/common/errors" 24 "go.chromium.org/luci/gae/service/datastore" 25 "go.chromium.org/luci/grpc/grpcutil" 26 ) 27 28 // taskRequestIDMask is xored with TaskRequest entity ID. 29 // 30 // Xoring with it flips first 63 bits of int64 (i.e. all of them except the most 31 // significant bit, which is used to represent a sign in int64: better to leave 32 // it alone). 33 // 34 // This allows to derive datastore keys from timestamps, but order them in 35 // reverse chronological order (most recent first). Without xoring we'd have 36 // to create a special index (because keys by default are ordered in increasing 37 // order). 38 const taskRequestIDMask = 0x7fffffffffffffff 39 40 // TaskIDVariant is an enum with possible variants of task ID encoding. 41 type TaskIDVariant int 42 43 const ( 44 // AsRequest instructs RequestKeyToTaskID to produce an ID ending with `0`. 45 AsRequest TaskIDVariant = 0 46 // AsRunResult instructs RequestKeyToTaskID to produce an ID ending with `1`. 47 AsRunResult TaskIDVariant = 1 48 ) 49 50 var ( 51 // BeginningOfTheWorld is used as beginning of time when constructing request 52 // keys: number of milliseconds since BeginningOfTheWorld is part of the key. 53 // 54 // The world started on 2010-01-01 at 00:00:00 UTC. The rationale is that 55 // using the original Unix epoch (1970) results in 40 years worth of key space 56 // wasted. 57 // 58 // We allocate 43 bits in the key for storing the timestamp at millisecond 59 // precision. This makes this scheme good for 2**43 / 365 / 24 / 3600 / 1000, 60 // which 278 years. We'll have until 2010+278 = 2288 before we run out of 61 // key space. Should be good enough for now. Can be fixed later. 62 BeginningOfTheWorld = time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC) 63 ) 64 65 // RequestKeyToTaskID converts TaskRequest entity key to a string form used in 66 // external APIs. 67 // 68 // For legacy reasons they are two flavors of string task IDs: 69 // 1. A "packed TaskRequest key", aka "packed TaskResultSummary" key. It is 70 // a hex string ending with 0, e.g. `6663cfc78b41fb10`. Pass AsRequest as 71 // the second argument to request this variant. 72 // 2. A "packed TaskRunResult key". It is a hex string ending with 1, e.g. 73 // `6663cfc78b41fb11`. Pass AsRunResult as the second argument to request 74 // this variant. 75 // 76 // Some APIs return the first form, others return the second. There's no clear 77 // logical reason why they do so anymore. They do it for backward compatibility 78 // with much older API, where these differences mattered. 79 // 80 // Panics if `key` is not a TaskRequest key. 81 func RequestKeyToTaskID(key *datastore.Key, variant TaskIDVariant) string { 82 if key.Kind() != "TaskRequest" { 83 panic(fmt.Sprintf("expecting TaskRequest key, but got %q", key.Kind())) 84 } 85 switch variant { 86 case AsRequest: 87 return fmt.Sprintf("%x0", key.IntID()^taskRequestIDMask) 88 case AsRunResult: 89 return fmt.Sprintf("%x1", key.IntID()^taskRequestIDMask) 90 default: 91 panic(fmt.Sprintf("invalid variant %d", variant)) 92 } 93 } 94 95 // TaskIDToRequestKey returns TaskRequest entity key given a task ID string. 96 // 97 // The task ID is something that looks like `6663cfc78b41fb10`, it is either 98 // a "packed TaskRequest key" (when ends with 0) or "a packed TaskRunResult key" 99 // (when ends with non-0). See RequestKeyToTaskID. 100 // 101 // Task request key is a root key of the hierarchy of entities representing 102 // a particular task. All key constructor functions for such entities take 103 // the request key as an argument. 104 func TaskIDToRequestKey(ctx context.Context, taskID string) (*datastore.Key, error) { 105 if err := checkIsHex(taskID, 2); err != nil { 106 return nil, errors.Annotate(err, "bad task ID").Tag(grpcutil.InvalidArgumentTag).Err() 107 } 108 // Chop the suffix byte. It is TaskRunResult index, we don't care about it. 109 num, err := strconv.ParseInt(taskID[:len(taskID)-1], 16, 64) 110 if err != nil { 111 return nil, errors.Annotate(err, "bad task ID").Tag(grpcutil.InvalidArgumentTag).Err() 112 } 113 return datastore.NewKey(ctx, "TaskRequest", "", num^taskRequestIDMask, nil), nil 114 } 115 116 // TimestampToRequestKey converts a timestamp to a request key. 117 // 118 // Note that this function does NOT accept a task id. This functions is 119 // primarily meant for limiting queries to a task creation time range. 120 func TimestampToRequestKey(ctx context.Context, timestamp time.Time, suffix int64) (*datastore.Key, error) { 121 if suffix < 0 || suffix > 0xffff { 122 return nil, errors.Reason("invalid suffix").Err() 123 } 124 deltaMS := timestamp.Sub(BeginningOfTheWorld).Milliseconds() 125 if deltaMS < 0 { 126 return nil, errors.Reason("time %s is before epoch %s", timestamp, BeginningOfTheWorld).Err() 127 } 128 base := deltaMS << 20 129 reqID := base | suffix<<4 | 0x1 130 return datastore.NewKey(ctx, "TaskRequest", "", reqID^taskRequestIDMask, nil), nil 131 }