vote._thing1 item = vote._thing2 if timer is None: timer = SimpleSillyStub() if not isinstance(item, (Link, Comment)): return if vote.valid_thing and not item._spam and not item._deleted: sr = item.subreddit_slow results = [] author = Account._byID(item.author_id) for sort in ('hot', 'top', 'controversial', 'new'): if isinstance(item, Link): results.append(get_submitted(author, sort, 'all')) if isinstance(item, Comment): results.append(get_comments(author, sort, 'all')) if isinstance(item, Link): # don't do 'new', because that was done by new_link, and # the time-‐filtered versions of top/controversial will be # done by mr_top results.extend([get_links(sr, 'hot', 'all'), get_links(sr, 'top', 'all'), get_links(sr, 'controversial', 'all’)]) parsed = utils.UrlParser(item.url) if parsed.hostname and not parsed.hostname.endswith('imgur.com'): for domain in parsed.domain_permutations(): for sort in ("hot", "top", "controversial"): results.append(get_domain_links(domain, sort, "all")) add_queries(results, insert_items = item, foreground=foreground) timer.intermediate("permacache”) if isinstance(item, Link): # must update both because we don't know if it's a changed # vote with CachedQueryMutator() as m: if vote._name == '1': m.insert(get_liked(user), [vote]) m.delete(get_disliked(user), [vote]) elif vote._name == '-‐1': m.delete(get_liked(user), [vote]) m.insert(get_disliked(user), [vote]) else: m.delete(get_liked(user), [vote]) m.delete(get_disliked(user), [vote]) def add_queries(queries, insert_items=None, delete_items=None, foreground=False): """Adds multiple queries to the query queue. If insert_items or delete_items is specified, the query may not need to be recomputed against the database.""" for q in queries: if insert_items and q.can_insert(): log.debug("Inserting %s into query %s" % (insert_items, q)) if foreground: q.insert(insert_items) else: worker.do(q.insert, insert_items) elif delete_items and q.can_delete(): log.debug("Deleting %s from query %s" % (delete_items, q)) if foreground: q.delete(delete_items) else: worker.do(q.delete, delete_items) else: raise Exception("Cannot update query %r!" % (q,)) # dual-‐write any queries that are being migrated to the new query cache with CachedQueryMutator() as m: new_queries = [getattr(q, 'new_query') for q in queries if hasattr(q, 'new_query')] if insert_items: for query in new_queries: m.insert(query, tup(insert_items)) if delete_items: for query in new_queries: m.delete(query, tup(delete_items)) class CachedResults(object): """Given a query returns a list-‐like object that will lazily look up the query from the persistent cache. """ def __init__(self, query, filter): self.query = query self.query._limit = precompute_limit self.filter = filter self.iden = self.query._iden() self.sort_cols = [s.col for s in self.query._sort] self.data = [] self._fetched = False @property def sort(self): return self.query._sort def fetch(self, force=False): """Loads the query from the cache.""" self.fetch_multi([self], force=force) @classmethod def fetch_multi(cls, crs, force=False): unfetched = filter(lambda cr: force or not cr._fetched, crs) if not unfetched: return cached = query_cache.get_multi([cr.iden for cr in unfetched], allow_local = not force) for cr in unfetched: cr.data = cached.get(cr.iden) or [] cr._fetched = True def make_item_tuple(self, item): """Given a single 'item' from the result of a query build the tuple that will be stored in the query cache. It is effectively the fullname of the item after passing through the filter plus the columns of the unfiltered item to sort by.""" filtered_item = self.filter(item) lst = [filtered_item._fullname] for col in self.sort_cols: #take the property of the original attr = getattr(item, col) #convert dates to epochs to take less space if isinstance(attr, datetime): attr = epoch_seconds(attr) lst.append(attr) return tuple(lst) def can_insert(self): """True if a new item can just be inserted rather than rerunning the query.""" # This is only true in some circumstances: queries where # eligibility in the list is determined only by its sort # value (e.g. hot) and where addition/removal from the list # incurs an insertion/deletion event called on the query. So # the top hottest items in X some subreddit where the query # is notified on every submission/banning/unbanning/deleting # will work, but for queries with a time-‐component or some # other eligibility factor, it cannot be inserted this way. if self.query._sort in ([desc('_date')], [desc('_hot'), desc('_date')], [desc('_score'), desc('_date')], [desc('_controversy'), desc('_date')]): if not any(r for r in self.query._rules if r.lval.name == '_date'): # if no time-‐rule is specified, then it's 'all' return True return False def can_delete(self): "True if a item can be removed from the listing, always true for now." return True def _mutate(self, fn, willread=True): self.data = query_cache.mutate(self.iden, fn, default=[], willread=willread) self._fetched=True def insert(self, items): """Inserts the item into the cached data. This only works under certain criteria, see can_insert.""" self._insert_tuples([self.make_item_tuple(item) for item in tup(items)]) def _insert_tuples(self, t): def _mutate(data): data = data or [] # short-‐circuit if we already know that no item to be # added qualifies to be stored. Since we know that this is # sorted descending by datum[1:], we can just check the # last item and see if we're smaller than it is if (len(data) >= precompute_limit and all(x[1:] < data[-‐1][1:] for x in t)): return data # insert the new items, remove the duplicates (keeping the # one being inserted over the stored value if applicable), # and sort the result newfnames = set(x[0] for x in t) data = filter(lambda x: x[0] not in newfnames, data) data.extend(t) data.sort(reverse=True, key=lambda x: x[1:]) if len(t) + len(data) > precompute_limit: data = data[:precompute_limit] return data self._mutate(_mutate) def delete(self, items): """Deletes an item from the cached data.""" fnames = set(self.filter(x)._fullname for x in tup(items)) def _mutate(data): data = data or [] return filter(lambda x: x[0] not in fnames, data) self._mutate(_mutate) def _replace(self, tuples): """Take pre-‐rendered tuples from mr_top and replace the contents of the query outright. This should be considered a private API""" def _mutate(data): return tuples self._mutate(_mutate, willread=False) def update(self): """Runs the query and stores the result in the cache. This is only run by hand.""" self.data = [self.make_item_tuple(i) for i in self.query] self._fetched = True query_cache.set(self.iden, self.data) def __repr__(self): return '<CachedResults %s %s>' % (self.query._rules, self.query._sort) def __iter__(self): self.fetch() for x in self.data: yield x[0] class MergedCachedResults(object): """Given two CachedResults, merges their lists based on the sorts of their queries.""" # normally we'd do this by having a superclass of CachedResults, # but we have legacy pickled CachedResults that we don't want to # break def __init__(self, results): self.cached_results = results CachedResults.fetch_multi([r for r in results if isinstance(r, CachedResults)]) CachedQuery._fetch_multi([r for r in results if isinstance(r, CachedQuery)]) self._fetched = True self.sort = results[0].sort comparator = ThingTupleComparator(self.sort) # make sure they're all the same assert all(r.sort == self.sort for r in results[1:]) all_items = [] for cr in results: all_items.extend(cr.data) all_items.sort(cmp=comparator) self.data = all_items hTps://github.com/reddit/reddit/blob/ master/r2/r2/lib/db/queries.py