go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/starlark/stdlib/internal/luci/lib/swarming.star (about) 1 # Copyright 2019 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 """Swarming related supporting structs and functions.""" 16 17 load("@stdlib//internal/validate.star", "validate") 18 load("@stdlib//internal/time.star", "time") 19 20 # A struct returned by swarming.cache(...). 21 # 22 # See swarming.cache(...) function for all details. 23 # 24 # Fields: 25 # path: string, where to mount the cache. 26 # name: string, name of the cache to mount. 27 # wait_for_warm_cache: duration or None, how long to wait for a warm cache. 28 _cache_ctor = __native__.genstruct("swarming.cache") 29 30 # A struct returned by swarming.dimension(...). 31 # 32 # See swarming.dimension(...) function for all details. 33 # 34 # Fields: 35 # value: string, value of the dimension. 36 # expiration: duration or None, when the dimension expires. 37 _dimension_ctor = __native__.genstruct("swarming.dimension") 38 39 def _cache(path, *, name = None, wait_for_warm_cache = None): 40 """Represents a request for the bot to mount a named cache to a path. 41 42 Each bot has a LRU of named caches: think of them as local named directories 43 in some protected place that survive between builds. 44 45 A build can request one or more such caches to be mounted (in read/write 46 mode) at the requested path relative to some known root. In recipes-based 47 builds, the path is relative to `api.paths['cache']` dir. 48 49 If it's the first time a cache is mounted on this particular bot, it will 50 appear as an empty directory. Otherwise it will contain whatever was left 51 there by the previous build that mounted exact same named cache on this bot, 52 even if that build is completely irrelevant to the current build and just 53 happened to use the same named cache (sometimes this is useful to share 54 state between different builders). 55 56 At the end of the build the cache directory is unmounted. If at that time 57 the bot is running out of space, caches (in their entirety, the named cache 58 directory and all files inside) are evicted in LRU manner until there's 59 enough free disk space left. Renaming a cache is equivalent to clearing it 60 from the builder perspective. The files will still be there, but eventually 61 will be purged by GC. 62 63 Additionally, Buildbucket always implicitly requests to mount a special 64 builder cache to 'builder' path: 65 66 swarming.cache('builder', name=some_hash('<project>/<bucket>/<builder>')) 67 68 This means that any LUCI builder has a "personal disk space" on the bot. 69 Builder cache is often a good start before customizing caching. In recipes, 70 it is available at `api.path['cache'].join('builder')`. 71 72 In order to share the builder cache directory among multiple builders, some 73 explicitly named cache can be mounted to `builder` path on these builders. 74 Buildbucket will not try to override it with its auto-generated builder 75 cache. 76 77 For example, if builders **A** and **B** both declare they use named cache 78 `swarming.cache('builder', name='my_shared_cache')`, and an **A** build ran 79 on a bot and left some files in the builder cache, then when a **B** build 80 runs on the same bot, the same files will be available in its builder cache. 81 82 If the pool of swarming bots is shared among multiple LUCI projects and 83 projects mount same named cache, the cache will be shared across projects. 84 To avoid affecting and being affected by other projects, prefix the cache 85 name with something project-specific, e.g. `v8-`. 86 87 Args: 88 path: path where the cache should be mounted to, relative to some known 89 root (in recipes this root is `api.path['cache']`). Must use POSIX 90 format (forward slashes). In most cases, it does not need slashes at 91 all. Must be unique in the given builder definition (cannot mount 92 multiple caches to the same path). Required. 93 name: identifier of the cache to mount to the path. Default is same value 94 as `path` itself. Must be unique in the given builder definition (cannot 95 mount the same cache to multiple paths). 96 wait_for_warm_cache: how long to wait (with minutes precision) for a bot 97 that has this named cache already to become available and pick up the 98 build, before giving up and starting looking for any matching bot 99 (regardless whether it has the cache or not). If there are no bots with 100 this cache at all, the build will skip waiting and will immediately 101 fallback to any matching bot. By default (if unset or zero), there'll be 102 no attempt to find a bot with this cache already warm: the build may or 103 may not end up on a warm bot, there's no guarantee one way or another. 104 105 Returns: 106 swarming.cache struct with fields `path`, `name` and `wait_for_warm_cache`. 107 """ 108 path = validate.string("path", path) 109 name = validate.string("name", name, default = path, required = False) 110 return _cache_ctor( 111 path = path, 112 name = name, 113 wait_for_warm_cache = validate.duration( 114 "wait_for_warm_cache", 115 wait_for_warm_cache, 116 precision = time.minute, 117 min = time.minute, 118 required = False, 119 ), 120 ) 121 122 def _dimension(value, *, expiration = None): 123 """A value of some Swarming dimension, annotated with its expiration time. 124 125 Intended to be used as a value in `dimensions` dict of luci.builder(...) 126 when using dimensions that expire: 127 128 ```python 129 luci.builder( 130 ... 131 dimensions = { 132 ... 133 'device': swarming.dimension('preferred', expiration=5*time.minute), 134 ... 135 }, 136 ... 137 ) 138 ``` 139 140 Args: 141 value: string value of the dimension. Required. 142 expiration: how long to wait (with minutes precision) for a bot with this 143 dimension to become available and pick up the build, or None to wait 144 until the overall build expiration timeout. 145 146 Returns: 147 swarming.dimension struct with fields `value` and `expiration`. 148 """ 149 return _dimension_ctor( 150 value = validate.string("value", value), 151 expiration = validate.duration( 152 "expiration", 153 expiration, 154 precision = time.minute, 155 min = time.minute, 156 required = False, 157 ), 158 ) 159 160 def _validate_caches(attr, caches): 161 """Validates a list of caches. 162 163 Ensures each entry is swarming.cache struct, and no two entries use same 164 name or path. 165 166 DocTags: 167 Advanced. 168 169 Args: 170 attr: field name with caches, for error messages. Required. 171 caches: a list of swarming.cache(...) entries to validate. Required. 172 173 Returns: 174 Validates list of caches (may be an empty list, never None). 175 """ 176 per_path = {} 177 per_name = {} 178 caches = validate.list(attr, caches) 179 for c in caches: 180 validate.struct(attr, c, _cache_ctor) 181 if c.path in per_path: 182 fail('bad "caches": caches %s and %s use same path' % (c, per_path[c.path])) 183 if c.name in per_name: 184 fail('bad "caches": caches %s and %s use same name' % (c, per_name[c.name])) 185 per_path[c.path] = c 186 per_name[c.name] = c 187 return caches 188 189 def _validate_dimensions(attr, dimensions, *, allow_none = False): 190 """Validates and normalizes a dict with dimensions. 191 192 The dict should have string keys and values are swarming.dimension, a string 193 or a list of thereof (for repeated dimensions). 194 195 DocTags: 196 Advanced. 197 198 Args: 199 attr: field name with dimensions, for error messages. Required. 200 dimensions: a dict `{string: string|swarming.dimension}`. Required. 201 allow_none: if True, allow None values (indicates absence of the dimension). 202 203 Returns: 204 Validated and normalized dict in form `{string: [swarming.dimension]}`. 205 """ 206 out = {} 207 for k, v in validate.str_dict(attr, dimensions).items(): 208 validate.string(attr, k) 209 if allow_none and v == None: 210 out[k] = None 211 elif type(v) == "list": 212 out[k] = [_as_dim(k, x) for x in v] 213 else: 214 out[k] = [_as_dim(k, v)] 215 return out 216 217 def _as_dim(key, val): 218 """string|swarming.dimension -> swarming.dimension.""" 219 if val == None: 220 fail("bad dimension %r: None value is not allowed" % key) 221 if type(val) == "string": 222 return _dimension(val) 223 return validate.struct(key, val, _dimension_ctor) 224 225 def _validate_tags(attr, tags): 226 """Validates a list of `k:v` pairs with Swarming tags. 227 228 DocTags: 229 Advanced. 230 231 Args: 232 attr: field name with tags, for error messages. Required. 233 tags: a list of tags to validate. Required. 234 235 Returns: 236 Validated list of tags in same order, with duplicates removed. 237 """ 238 out = set() # note: in starlark sets/dicts remember the order 239 for t in validate.list(attr, tags): 240 validate.string(attr, t, regexp = r".+\:.+") 241 out = out.union([t]) 242 return list(out) 243 244 swarming = struct( 245 cache = _cache, 246 dimension = _dimension, 247 248 # Validators are useful for macros that modify caches, dimensions, etc. 249 validate_caches = _validate_caches, 250 validate_dimensions = _validate_dimensions, 251 validate_tags = _validate_tags, 252 )