Coverage for /builds/BuildGrid/buildgrid/buildgrid/server/actioncache/caches/lru_cache.py: 100.00%

48 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-06-11 15:37 +0000

1# Copyright (C) 2020 Bloomberg LP 

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 

16import collections 

17import logging 

18from typing import Tuple 

19 

20from buildgrid._exceptions import NotFoundError 

21from buildgrid._protos.build.bazel.remote.execution.v2 import remote_execution_pb2 

22from buildgrid._protos.build.bazel.remote.execution.v2.remote_execution_pb2 import ActionResult, Digest 

23from buildgrid.server.actioncache.caches.action_cache_abc import ActionCacheABC 

24from buildgrid.server.cas.storage.storage_abc import StorageABC 

25from buildgrid.server.metrics_names import AC_UNUSABLE_CACHE_HITS_METRIC_NAME 

26from buildgrid.server.metrics_utils import publish_counter_metric 

27 

28LOGGER = logging.getLogger(__name__) 

29 

30 

31class LruActionCache(ActionCacheABC): 

32 """In-memory Action Cache implementation with LRU eviction. 

33 

34 This cache has a configurable fixed size, evicting the least recently 

35 accessed entry when adding a new entry would exceed the fixed size. The 

36 cache is entirely stored in memory so its contents are lost on restart. 

37 

38 This type of cache is ideal for use cases that need a simple and fast 

39 cache, with no requirements for longevity of the cache content. It is not 

40 recommended to use this type of cache in situations where you may wish to 

41 obtain cached results a reasonable time in the future, due to its fixed 

42 size. 

43 

44 """ 

45 

46 def __init__( 

47 self, storage: StorageABC, max_cached_refs: int, allow_updates: bool = True, cache_failed_actions: bool = True 

48 ): 

49 """Initialise a new in-memory LRU Action Cache. 

50 

51 Args: 

52 storage (StorageABC): Storage backend instance to be used. 

53 max_cached_refs (int): Maximum number of entries to store in the cache. 

54 allow_updates (bool): Whether to allow writing to the cache. If false, 

55 this is a read-only cache for all clients. 

56 cache_failed_actions (bool): Whether or not to cache Actions with 

57 non-zero exit codes. 

58 

59 """ 

60 super().__init__(storage=storage) 

61 

62 self._cache_failed_actions = cache_failed_actions 

63 self._storage = storage 

64 self._allow_updates = allow_updates 

65 self._max_cached_refs = max_cached_refs 

66 self._digest_map = collections.OrderedDict() # type: ignore 

67 

68 def get_action_result(self, action_digest: Digest) -> ActionResult: 

69 """Retrieves the cached result for an Action. 

70 

71 If there is no cached result found, returns None. 

72 

73 Args: 

74 action_digest (Digest): The digest of the Action to retrieve the 

75 cached result of. 

76 

77 """ 

78 key = self._get_key(action_digest) 

79 if key in self._digest_map: 

80 assert self._storage, "Storage used before initialization" 

81 action_result = self._storage.get_message(self._digest_map[key], remote_execution_pb2.ActionResult) 

82 

83 if action_result is not None: 

84 if self._referenced_blobs_still_exist(action_digest, action_result): 

85 self._digest_map.move_to_end(key) 

86 return action_result 

87 

88 publish_counter_metric(AC_UNUSABLE_CACHE_HITS_METRIC_NAME, 1, {"instance-name": self._instance_name}) 

89 

90 if self._allow_updates: 

91 LOGGER.debug( 

92 f"Removing {action_digest.hash}/{action_digest.size_bytes}" 

93 "from cache due to missing blobs in CAS" 

94 ) 

95 del self._digest_map[key] 

96 

97 raise NotFoundError(f"Key not found: {key}") 

98 

99 def update_action_result(self, action_digest: Digest, action_result: ActionResult) -> None: 

100 """Stores a result for an Action in the cache. 

101 

102 If the result has a non-zero exit code and `cache_failed_actions` is False 

103 for this cache, the result is not cached. 

104 

105 Args: 

106 action_digest (Digest): The digest of the Action whose result is 

107 being cached. 

108 action_result (ActionResult): The result to cache for the given 

109 Action digest. 

110 

111 """ 

112 if self._cache_failed_actions or action_result.exit_code == 0: 

113 key = self._get_key(action_digest) 

114 if not self._allow_updates: 

115 raise NotImplementedError("Updating cache not allowed") 

116 

117 if self._max_cached_refs == 0: 

118 return 

119 

120 while len(self._digest_map) >= self._max_cached_refs: 

121 self._digest_map.popitem(last=False) 

122 

123 assert self._storage, "Storage used before initialization" 

124 result_digest = self._storage.put_message(action_result) 

125 self._digest_map[key] = result_digest 

126 

127 LOGGER.info(f"Result cached for action [{action_digest.hash}/{action_digest.size_bytes}]") 

128 

129 def _get_key(self, action_digest: Digest) -> Tuple[str, int]: 

130 """Get a hashable cache key from a given Action digest. 

131 

132 Args: 

133 action_digest (Digest): The digest to produce a cache key for. 

134 

135 """ 

136 return (action_digest.hash, action_digest.size_bytes)