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  }