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