blob: bb92e73e18f2a62bbd1c60bd4d7aa5b97f857d42 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""A set of functions that provide persistence for stars.
7
8Stars can be on users, projects, or issues.
9"""
10from __future__ import print_function
11from __future__ import division
12from __future__ import absolute_import
13
14import logging
15
16import settings
17from features import filterrules_helpers
18from framework import sql
19from services import caches
20
21
22USERSTAR_TABLE_NAME = 'UserStar'
23PROJECTSTAR_TABLE_NAME = 'ProjectStar'
24ISSUESTAR_TABLE_NAME = 'IssueStar'
25HOTLISTSTAR_TABLE_NAME = 'HotlistStar'
26
27# TODO(jrobbins): Consider adding memcache here if performance testing shows
28# that stars are a bottleneck. Keep in mind that issue star counts are
29# already denormalized and stored in the Issue, which is cached in memcache.
30
31
32class AbstractStarService(object):
33 """The persistence layer for any kind of star data."""
34
35 def __init__(self, cache_manager, tbl, item_col, user_col, cache_kind):
36 """Constructor.
37
38 Args:
39 cache_manager: local cache with distributed invalidation.
40 tbl: SQL table that stores star data.
41 item_col: string SQL column name that holds int item IDs.
42 user_col: string SQL column name that holds int user IDs
43 of the user who starred the item.
44 cache_kind: string saying the kind of RAM cache.
45 """
46 self.tbl = tbl
47 self.item_col = item_col
48 self.user_col = user_col
49
50 # Items starred by users, keyed by user who did the starring.
51 self.star_cache = caches.RamCache(cache_manager, 'user')
52 # Users that starred an item, keyed by item ID.
53 self.starrer_cache = caches.RamCache(cache_manager, cache_kind)
54 # Counts of the users that starred an item, keyed by item ID.
55 self.star_count_cache = caches.RamCache(cache_manager, cache_kind)
56
57 def ExpungeStars(self, cnxn, item_id, commit=True, limit=None):
58 """Wipes an item's stars from the system."""
59 self.tbl.Delete(
60 cnxn, commit=commit, limit=limit, **{self.item_col: item_id})
61
62 def ExpungeStarsByUsers(self, cnxn, user_ids, limit=None):
63 """Wipes a user's stars from the system.
64 This method will not commit the operation. This method will
65 not make changes to in-memory data.
66 """
67 self.tbl.Delete(cnxn, user_id=user_ids, commit=False, limit=limit)
68
69 def LookupItemStarrers(self, cnxn, item_id):
70 """Returns list of users having stars on the specified item."""
71 starrer_list_dict = self.LookupItemsStarrers(cnxn, [item_id])
72 return starrer_list_dict[item_id]
73
74 def LookupItemsStarrers(self, cnxn, items_ids):
75 """Returns {item_id: [uid, ...]} of users who starred these items."""
76 starrer_list_dict, missed_ids = self.starrer_cache.GetAll(items_ids)
77
78 if missed_ids:
79 rows = self.tbl.Select(
80 cnxn, cols=[self.item_col, self.user_col],
81 **{self.item_col: missed_ids})
82 # Ensure that every requested item_id has an entry so that even
83 # zero-star items get cached.
84 retrieved_starrers = {item_id: [] for item_id in missed_ids}
85 for item_id, starrer_id in rows:
86 retrieved_starrers[item_id].append(starrer_id)
87 starrer_list_dict.update(retrieved_starrers)
88 self.starrer_cache.CacheAll(retrieved_starrers)
89
90 return starrer_list_dict
91
92 def LookupStarredItemIDs(self, cnxn, starrer_user_id):
93 """Returns list of item IDs that were starred by the specified user."""
94 if not starrer_user_id:
95 return [] # Anon user cannot star anything.
96
97 cached_item_ids = self.star_cache.GetItem(starrer_user_id)
98 if cached_item_ids is not None:
99 return cached_item_ids
100
101 rows = self.tbl.Select(cnxn, cols=[self.item_col], user_id=starrer_user_id)
102 starred_ids = [row[0] for row in rows]
103 self.star_cache.CacheItem(starrer_user_id, starred_ids)
104 return starred_ids
105
106 def IsItemStarredBy(self, cnxn, item_id, starrer_user_id):
107 """Return True if the given issue is starred by the given user."""
108 starred_ids = self.LookupStarredItemIDs(cnxn, starrer_user_id)
109 return item_id in starred_ids
110
111 def CountItemStars(self, cnxn, item_id):
112 """Returns the number of stars on the specified item."""
113 count_dict = self.CountItemsStars(cnxn, [item_id])
114 return count_dict.get(item_id, 0)
115
116 def CountItemsStars(self, cnxn, item_ids):
117 """Get a dict {item_id: count} for the given items."""
118 item_count_dict, missed_ids = self.star_count_cache.GetAll(item_ids)
119
120 if missed_ids:
121 rows = self.tbl.Select(
122 cnxn, cols=[self.item_col, 'COUNT(%s)' % self.user_col],
123 group_by=[self.item_col],
124 **{self.item_col: missed_ids})
125 # Ensure that every requested item_id has an entry so that even
126 # zero-star items get cached.
127 retrieved_counts = {item_id: 0 for item_id in missed_ids}
128 retrieved_counts.update(rows)
129 item_count_dict.update(retrieved_counts)
130 self.star_count_cache.CacheAll(retrieved_counts)
131
132 return item_count_dict
133
134 def _SetStarsBatch(
135 self, cnxn, item_id, starrer_user_ids, starred, commit=True):
136 """Sets or unsets stars for the specified item and users."""
137 if starred:
138 rows = [(item_id, user_id) for user_id in starrer_user_ids]
139 self.tbl.InsertRows(
140 cnxn, [self.item_col, self.user_col], rows, ignore=True,
141 commit=commit)
142 else:
143 self.tbl.Delete(
144 cnxn, commit=commit,
145 **{self.item_col: item_id, self.user_col: starrer_user_ids})
146
147 self.star_cache.InvalidateKeys(cnxn, starrer_user_ids)
148 self.starrer_cache.Invalidate(cnxn, item_id)
149 self.star_count_cache.Invalidate(cnxn, item_id)
150
151 def SetStarsBatch(
152 self, cnxn, item_id, starrer_user_ids, starred, commit=True):
153 """Sets or unsets stars for the specified item and users."""
154 self._SetStarsBatch(
155 cnxn, item_id, starrer_user_ids, starred, commit=commit)
156
157 def SetStar(self, cnxn, item_id, starrer_user_id, starred):
158 """Sets or unsets a star for the specified item and user."""
159 self._SetStarsBatch(cnxn, item_id, [starrer_user_id], starred)
160
161
162
163class UserStarService(AbstractStarService):
164 """Star service for stars on users."""
165
166 def __init__(self, cache_manager):
167 tbl = sql.SQLTableManager(USERSTAR_TABLE_NAME)
168 super(UserStarService, self).__init__(
169 cache_manager, tbl, 'starred_user_id', 'user_id', 'user')
170
171
172class ProjectStarService(AbstractStarService):
173 """Star service for stars on projects."""
174
175 def __init__(self, cache_manager):
176 tbl = sql.SQLTableManager(PROJECTSTAR_TABLE_NAME)
177 super(ProjectStarService, self).__init__(
178 cache_manager, tbl, 'project_id', 'user_id', 'project')
179
180
181class HotlistStarService(AbstractStarService):
182 """Star service for stars on hotlists."""
183
184 def __init__(self, cache_manager):
185 tbl = sql.SQLTableManager(HOTLISTSTAR_TABLE_NAME)
186 super(HotlistStarService, self).__init__(
187 cache_manager, tbl, 'hotlist_id', 'user_id', 'hotlist')
188
189
190class IssueStarService(AbstractStarService):
191 """Star service for stars on issues."""
192
193 def __init__(self, cache_manager):
194 tbl = sql.SQLTableManager(ISSUESTAR_TABLE_NAME)
195 super(IssueStarService, self).__init__(
196 cache_manager, tbl, 'issue_id', 'user_id', 'issue')
197
198 # pylint: disable=arguments-differ
199 def SetStar(
200 self, cnxn, services, config, issue_id, starrer_user_id, starred):
201 """Add or remove a star on the given issue for the given user.
202
203 Args:
204 cnxn: connection to SQL database.
205 services: connections to persistence layer.
206 config: ProjectIssueConfig PB for the project containing the issue.
207 issue_id: integer global ID of an issue.
208 starrer_user_id: user ID of the user who starred the issue.
209 starred: boolean True for adding a star, False when removing one.
210 """
211 self.SetStarsBatch(
212 cnxn, services, config, issue_id, [starrer_user_id], starred)
213
214 # pylint: disable=arguments-differ
215 def SetStarsBatch(
216 self, cnxn, services, config, issue_id, starrer_user_ids, starred):
217 """Add or remove a star on the given issue for the given users.
218
219 Args:
220 cnxn: connection to SQL database.
221 services: connections to persistence layer.
222 config: ProjectIssueConfig PB for the project containing the issue.
223 issue_id: integer global ID of an issue.
224 starrer_user_id: user ID of the user who starred the issue.
225 starred: boolean True for adding a star, False when removing one.
226 """
227 logging.info(
228 'SetStarsBatch:%r, %r, %r', issue_id, starrer_user_ids, starred)
229 super(IssueStarService, self).SetStarsBatch(
230 cnxn, issue_id, starrer_user_ids, starred)
231
232 # Because we will modify issues, load from DB rather than cache.
233 issue = services.issue.GetIssue(cnxn, issue_id, use_cache=False)
234 issue.star_count = self.CountItemStars(cnxn, issue_id)
235 filterrules_helpers.ApplyFilterRules(cnxn, services, issue, config)
236 # Note: only star_count could change due to the starring, but any
237 # field could have changed as a result of filter rules.
238 services.issue.UpdateIssue(cnxn, issue)
239
240 self.star_cache.InvalidateKeys(cnxn, starrer_user_ids)
241 self.starrer_cache.Invalidate(cnxn, issue_id)
242
243 # TODO(crbug.com/monorail/8098): This method should replace SetStarsBatch.
244 # New code should be calling SetStarsBatch_SkipIssueUpdate.
245 # SetStarsBatch, does issue.star_count updating that should be done
246 # in the business logic layer instead. E.g. We can create a
247 # WorkEnv.BatchSetStars() that includes the star_count updating work.
248 def SetStarsBatch_SkipIssueUpdate(
249 self, cnxn, issue_id, starrer_user_ids, starred, commit=True):
250 # type: (MonorailConnection, int, Sequence[int], bool, Optional[bool])
251 # -> None
252 """Add or remove a star on the given issue for the given users.
253
254 Note: unlike SetStarsBatch above, does not make any updates to the
255 the issue itself e.g. updating issue.star_count.
256
257 """
258 logging.info(
259 'SetStarsBatch:%r, %r, %r', issue_id, starrer_user_ids, starred)
260 super(IssueStarService, self).SetStarsBatch(
261 cnxn, issue_id, starrer_user_ids, starred, commit=commit)
262
263 self.star_cache.InvalidateKeys(cnxn, starrer_user_ids)
264 self.starrer_cache.Invalidate(cnxn, issue_id)