851 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			851 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			Python
		
	
	
| # -*- coding: utf-8 -*-
 | |
| # Copyright 2014-2016 OpenMarket Ltd
 | |
| #
 | |
| # Licensed under the Apache License, Version 2.0 (the "License");
 | |
| # you may not use this file except in compliance with the License.
 | |
| # You may obtain a copy of the License at
 | |
| #
 | |
| #     http://www.apache.org/licenses/LICENSE-2.0
 | |
| #
 | |
| # Unless required by applicable law or agreed to in writing, software
 | |
| # distributed under the License is distributed on an "AS IS" BASIS,
 | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| # See the License for the specific language governing permissions and
 | |
| # limitations under the License.
 | |
| 
 | |
| from ._base import SQLBaseStore
 | |
| from synapse.util.caches.descriptors import cached, cachedList
 | |
| from synapse.util.caches import intern_string
 | |
| from synapse.storage.engines import PostgresEngine
 | |
| 
 | |
| from twisted.internet import defer
 | |
| 
 | |
| import logging
 | |
| 
 | |
| logger = logging.getLogger(__name__)
 | |
| 
 | |
| 
 | |
| MAX_STATE_DELTA_HOPS = 100
 | |
| 
 | |
| 
 | |
| class StateStore(SQLBaseStore):
 | |
|     """ Keeps track of the state at a given event.
 | |
| 
 | |
|     This is done by the concept of `state groups`. Every event is a assigned
 | |
|     a state group (identified by an arbitrary string), which references a
 | |
|     collection of state events. The current state of an event is then the
 | |
|     collection of state events referenced by the event's state group.
 | |
| 
 | |
|     Hence, every change in the current state causes a new state group to be
 | |
|     generated. However, if no change happens (e.g., if we get a message event
 | |
|     with only one parent it inherits the state group from its parent.)
 | |
| 
 | |
|     There are three tables:
 | |
|       * `state_groups`: Stores group name, first event with in the group and
 | |
|         room id.
 | |
|       * `event_to_state_groups`: Maps events to state groups.
 | |
|       * `state_groups_state`: Maps state group to state events.
 | |
|     """
 | |
| 
 | |
|     STATE_GROUP_DEDUPLICATION_UPDATE_NAME = "state_group_state_deduplication"
 | |
|     STATE_GROUP_INDEX_UPDATE_NAME = "state_group_state_type_index"
 | |
| 
 | |
|     def __init__(self, hs):
 | |
|         super(StateStore, self).__init__(hs)
 | |
|         self.register_background_update_handler(
 | |
|             self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME,
 | |
|             self._background_deduplicate_state,
 | |
|         )
 | |
|         self.register_background_update_handler(
 | |
|             self.STATE_GROUP_INDEX_UPDATE_NAME,
 | |
|             self._background_index_state,
 | |
|         )
 | |
| 
 | |
|     @defer.inlineCallbacks
 | |
|     def get_state_groups_ids(self, room_id, event_ids):
 | |
|         if not event_ids:
 | |
|             defer.returnValue({})
 | |
| 
 | |
|         event_to_groups = yield self._get_state_group_for_events(
 | |
|             event_ids,
 | |
|         )
 | |
| 
 | |
|         groups = set(event_to_groups.values())
 | |
|         group_to_state = yield self._get_state_for_groups(groups)
 | |
| 
 | |
|         defer.returnValue(group_to_state)
 | |
| 
 | |
|     @defer.inlineCallbacks
 | |
|     def get_state_groups(self, room_id, event_ids):
 | |
|         """ Get the state groups for the given list of event_ids
 | |
| 
 | |
|         The return value is a dict mapping group names to lists of events.
 | |
|         """
 | |
|         if not event_ids:
 | |
|             defer.returnValue({})
 | |
| 
 | |
|         group_to_ids = yield self.get_state_groups_ids(room_id, event_ids)
 | |
| 
 | |
|         state_event_map = yield self.get_events(
 | |
|             [
 | |
|                 ev_id for group_ids in group_to_ids.values()
 | |
|                 for ev_id in group_ids.values()
 | |
|             ],
 | |
|             get_prev_content=False
 | |
|         )
 | |
| 
 | |
|         defer.returnValue({
 | |
|             group: [
 | |
|                 state_event_map[v] for v in event_id_map.values() if v in state_event_map
 | |
|             ]
 | |
|             for group, event_id_map in group_to_ids.items()
 | |
|         })
 | |
| 
 | |
|     def _have_persisted_state_group_txn(self, txn, state_group):
 | |
|         txn.execute(
 | |
|             "SELECT count(*) FROM state_groups WHERE id = ?",
 | |
|             (state_group,)
 | |
|         )
 | |
|         row = txn.fetchone()
 | |
|         return row and row[0]
 | |
| 
 | |
|     def _store_mult_state_groups_txn(self, txn, events_and_contexts):
 | |
|         state_groups = {}
 | |
|         for event, context in events_and_contexts:
 | |
|             if event.internal_metadata.is_outlier():
 | |
|                 continue
 | |
| 
 | |
|             if context.current_state_ids is None:
 | |
|                 continue
 | |
| 
 | |
|             state_groups[event.event_id] = context.state_group
 | |
| 
 | |
|             if self._have_persisted_state_group_txn(txn, context.state_group):
 | |
|                 continue
 | |
| 
 | |
|             self._simple_insert_txn(
 | |
|                 txn,
 | |
|                 table="state_groups",
 | |
|                 values={
 | |
|                     "id": context.state_group,
 | |
|                     "room_id": event.room_id,
 | |
|                     "event_id": event.event_id,
 | |
|                 },
 | |
|             )
 | |
| 
 | |
|             # We persist as a delta if we can, while also ensuring the chain
 | |
|             # of deltas isn't tooo long, as otherwise read performance degrades.
 | |
|             if context.prev_group:
 | |
|                 potential_hops = self._count_state_group_hops_txn(
 | |
|                     txn, context.prev_group
 | |
|                 )
 | |
|             if context.prev_group and potential_hops < MAX_STATE_DELTA_HOPS:
 | |
|                 self._simple_insert_txn(
 | |
|                     txn,
 | |
|                     table="state_group_edges",
 | |
|                     values={
 | |
|                         "state_group": context.state_group,
 | |
|                         "prev_state_group": context.prev_group,
 | |
|                     },
 | |
|                 )
 | |
| 
 | |
|                 self._simple_insert_many_txn(
 | |
|                     txn,
 | |
|                     table="state_groups_state",
 | |
|                     values=[
 | |
|                         {
 | |
|                             "state_group": context.state_group,
 | |
|                             "room_id": event.room_id,
 | |
|                             "type": key[0],
 | |
|                             "state_key": key[1],
 | |
|                             "event_id": state_id,
 | |
|                         }
 | |
|                         for key, state_id in context.delta_ids.items()
 | |
|                     ],
 | |
|                 )
 | |
|             else:
 | |
|                 self._simple_insert_many_txn(
 | |
|                     txn,
 | |
|                     table="state_groups_state",
 | |
|                     values=[
 | |
|                         {
 | |
|                             "state_group": context.state_group,
 | |
|                             "room_id": event.room_id,
 | |
|                             "type": key[0],
 | |
|                             "state_key": key[1],
 | |
|                             "event_id": state_id,
 | |
|                         }
 | |
|                         for key, state_id in context.current_state_ids.items()
 | |
|                     ],
 | |
|                 )
 | |
| 
 | |
|         self._simple_insert_many_txn(
 | |
|             txn,
 | |
|             table="event_to_state_groups",
 | |
|             values=[
 | |
|                 {
 | |
|                     "state_group": state_group_id,
 | |
|                     "event_id": event_id,
 | |
|                 }
 | |
|                 for event_id, state_group_id in state_groups.items()
 | |
|             ],
 | |
|         )
 | |
| 
 | |
|     def _count_state_group_hops_txn(self, txn, state_group):
 | |
|         """Given a state group, count how many hops there are in the tree.
 | |
| 
 | |
|         This is used to ensure the delta chains don't get too long.
 | |
|         """
 | |
|         if isinstance(self.database_engine, PostgresEngine):
 | |
|             sql = ("""
 | |
|                 WITH RECURSIVE state(state_group) AS (
 | |
|                     VALUES(?::bigint)
 | |
|                     UNION ALL
 | |
|                     SELECT prev_state_group FROM state_group_edges e, state s
 | |
|                     WHERE s.state_group = e.state_group
 | |
|                 )
 | |
|                 SELECT count(*) FROM state;
 | |
|             """)
 | |
| 
 | |
|             txn.execute(sql, (state_group,))
 | |
|             row = txn.fetchone()
 | |
|             if row and row[0]:
 | |
|                 return row[0]
 | |
|             else:
 | |
|                 return 0
 | |
|         else:
 | |
|             # We don't use WITH RECURSIVE on sqlite3 as there are distributions
 | |
|             # that ship with an sqlite3 version that doesn't support it (e.g. wheezy)
 | |
|             next_group = state_group
 | |
|             count = 0
 | |
| 
 | |
|             while next_group:
 | |
|                 next_group = self._simple_select_one_onecol_txn(
 | |
|                     txn,
 | |
|                     table="state_group_edges",
 | |
|                     keyvalues={"state_group": next_group},
 | |
|                     retcol="prev_state_group",
 | |
|                     allow_none=True,
 | |
|                 )
 | |
|                 if next_group:
 | |
|                     count += 1
 | |
| 
 | |
|             return count
 | |
| 
 | |
|     @defer.inlineCallbacks
 | |
|     def get_current_state(self, room_id, event_type=None, state_key=""):
 | |
|         if event_type and state_key is not None:
 | |
|             result = yield self.get_current_state_for_key(
 | |
|                 room_id, event_type, state_key
 | |
|             )
 | |
|             defer.returnValue(result)
 | |
| 
 | |
|         def f(txn):
 | |
|             sql = (
 | |
|                 "SELECT event_id FROM current_state_events"
 | |
|                 " WHERE room_id = ? "
 | |
|             )
 | |
| 
 | |
|             if event_type and state_key is not None:
 | |
|                 sql += " AND type = ? AND state_key = ? "
 | |
|                 args = (room_id, event_type, state_key)
 | |
|             elif event_type:
 | |
|                 sql += " AND type = ?"
 | |
|                 args = (room_id, event_type)
 | |
|             else:
 | |
|                 args = (room_id, )
 | |
| 
 | |
|             txn.execute(sql, args)
 | |
|             results = txn.fetchall()
 | |
| 
 | |
|             return [r[0] for r in results]
 | |
| 
 | |
|         event_ids = yield self.runInteraction("get_current_state", f)
 | |
|         events = yield self._get_events(event_ids, get_prev_content=False)
 | |
|         defer.returnValue(events)
 | |
| 
 | |
|     @defer.inlineCallbacks
 | |
|     def get_current_state_for_key(self, room_id, event_type, state_key):
 | |
|         event_ids = yield self._get_current_state_for_key(room_id, event_type, state_key)
 | |
|         events = yield self._get_events(event_ids, get_prev_content=False)
 | |
|         defer.returnValue(events)
 | |
| 
 | |
|     @cached(num_args=3)
 | |
|     def _get_current_state_for_key(self, room_id, event_type, state_key):
 | |
|         def f(txn):
 | |
|             sql = (
 | |
|                 "SELECT event_id FROM current_state_events"
 | |
|                 " WHERE room_id = ? AND type = ? AND state_key = ?"
 | |
|             )
 | |
| 
 | |
|             args = (room_id, event_type, state_key)
 | |
|             txn.execute(sql, args)
 | |
|             results = txn.fetchall()
 | |
|             return [r[0] for r in results]
 | |
|         return self.runInteraction("get_current_state_for_key", f)
 | |
| 
 | |
|     @cached(num_args=2, max_entries=1000)
 | |
|     def _get_state_group_from_group(self, group, types):
 | |
|         raise NotImplementedError()
 | |
| 
 | |
|     @cachedList(cached_method_name="_get_state_group_from_group",
 | |
|                 list_name="groups", num_args=2, inlineCallbacks=True)
 | |
|     def _get_state_groups_from_groups(self, groups, types):
 | |
|         """Returns dictionary state_group -> (dict of (type, state_key) -> event id)
 | |
|         """
 | |
|         results = {}
 | |
| 
 | |
|         chunks = [groups[i:i + 100] for i in xrange(0, len(groups), 100)]
 | |
|         for chunk in chunks:
 | |
|             res = yield self.runInteraction(
 | |
|                 "_get_state_groups_from_groups",
 | |
|                 self._get_state_groups_from_groups_txn, chunk, types,
 | |
|             )
 | |
|             results.update(res)
 | |
| 
 | |
|         defer.returnValue(results)
 | |
| 
 | |
|     def _get_state_groups_from_groups_txn(self, txn, groups, types=None):
 | |
|         results = {group: {} for group in groups}
 | |
|         if isinstance(self.database_engine, PostgresEngine):
 | |
|             # Temporarily disable sequential scans in this transaction. This is
 | |
|             # a temporary hack until we can add the right indices in
 | |
|             txn.execute("SET LOCAL enable_seqscan=off")
 | |
| 
 | |
|             # The below query walks the state_group tree so that the "state"
 | |
|             # table includes all state_groups in the tree. It then joins
 | |
|             # against `state_groups_state` to fetch the latest state.
 | |
|             # It assumes that previous state groups are always numerically
 | |
|             # lesser.
 | |
|             # The PARTITION is used to get the event_id in the greatest state
 | |
|             # group for the given type, state_key.
 | |
|             # This may return multiple rows per (type, state_key), but last_value
 | |
|             # should be the same.
 | |
|             sql = ("""
 | |
|                 WITH RECURSIVE state(state_group) AS (
 | |
|                     VALUES(?::bigint)
 | |
|                     UNION ALL
 | |
|                     SELECT prev_state_group FROM state_group_edges e, state s
 | |
|                     WHERE s.state_group = e.state_group
 | |
|                 )
 | |
|                 SELECT type, state_key, last_value(event_id) OVER (
 | |
|                     PARTITION BY type, state_key ORDER BY state_group ASC
 | |
|                     ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
 | |
|                 ) AS event_id FROM state_groups_state
 | |
|                 WHERE state_group IN (
 | |
|                     SELECT state_group FROM state
 | |
|                 )
 | |
|                 %s
 | |
|             """)
 | |
| 
 | |
|             # Turns out that postgres doesn't like doing a list of OR's and
 | |
|             # is about 1000x slower, so we just issue a query for each specific
 | |
|             # type seperately.
 | |
|             if types:
 | |
|                 clause_to_args = [
 | |
|                     (
 | |
|                         "AND type = ? AND state_key = ?",
 | |
|                         (etype, state_key)
 | |
|                     )
 | |
|                     for etype, state_key in types
 | |
|                 ]
 | |
|             else:
 | |
|                 # If types is None we fetch all the state, and so just use an
 | |
|                 # empty where clause with no extra args.
 | |
|                 clause_to_args = [("", [])]
 | |
| 
 | |
|             for where_clause, where_args in clause_to_args:
 | |
|                 for group in groups:
 | |
|                     args = [group]
 | |
|                     args.extend(where_args)
 | |
| 
 | |
|                     txn.execute(sql % (where_clause,), args)
 | |
|                     rows = self.cursor_to_dict(txn)
 | |
|                     for row in rows:
 | |
|                         key = (row["type"], row["state_key"])
 | |
|                         results[group][key] = row["event_id"]
 | |
|         else:
 | |
|             if types is not None:
 | |
|                 where_clause = "AND (%s)" % (
 | |
|                     " OR ".join(["(type = ? AND state_key = ?)"] * len(types)),
 | |
|                 )
 | |
|             else:
 | |
|                 where_clause = ""
 | |
| 
 | |
|             # We don't use WITH RECURSIVE on sqlite3 as there are distributions
 | |
|             # that ship with an sqlite3 version that doesn't support it (e.g. wheezy)
 | |
|             for group in groups:
 | |
|                 group_tree = [group]
 | |
|                 next_group = group
 | |
| 
 | |
|                 while next_group:
 | |
|                     next_group = self._simple_select_one_onecol_txn(
 | |
|                         txn,
 | |
|                         table="state_group_edges",
 | |
|                         keyvalues={"state_group": next_group},
 | |
|                         retcol="prev_state_group",
 | |
|                         allow_none=True,
 | |
|                     )
 | |
|                     if next_group:
 | |
|                         group_tree.append(next_group)
 | |
| 
 | |
|                 sql = ("""
 | |
|                     SELECT type, state_key, event_id FROM state_groups_state
 | |
|                     INNER JOIN (
 | |
|                         SELECT type, state_key, max(state_group) as state_group
 | |
|                         FROM state_groups_state
 | |
|                         WHERE state_group IN (%s) %s
 | |
|                         GROUP BY type, state_key
 | |
|                     ) USING (type, state_key, state_group);
 | |
|                 """) % (",".join("?" for _ in group_tree), where_clause,)
 | |
| 
 | |
|                 args = list(group_tree)
 | |
|                 if types is not None:
 | |
|                     args.extend([i for typ in types for i in typ])
 | |
| 
 | |
|                 txn.execute(sql, args)
 | |
|                 rows = self.cursor_to_dict(txn)
 | |
|                 for row in rows:
 | |
|                     key = (row["type"], row["state_key"])
 | |
|                     results[group][key] = row["event_id"]
 | |
| 
 | |
|         return results
 | |
| 
 | |
|     @defer.inlineCallbacks
 | |
|     def get_state_for_events(self, event_ids, types):
 | |
|         """Given a list of event_ids and type tuples, return a list of state
 | |
|         dicts for each event. The state dicts will only have the type/state_keys
 | |
|         that are in the `types` list.
 | |
| 
 | |
|         Args:
 | |
|             event_ids (list)
 | |
|             types (list): List of (type, state_key) tuples which are used to
 | |
|                 filter the state fetched. `state_key` may be None, which matches
 | |
|                 any `state_key`
 | |
| 
 | |
|         Returns:
 | |
|             deferred: A list of dicts corresponding to the event_ids given.
 | |
|             The dicts are mappings from (type, state_key) -> state_events
 | |
|         """
 | |
|         event_to_groups = yield self._get_state_group_for_events(
 | |
|             event_ids,
 | |
|         )
 | |
| 
 | |
|         groups = set(event_to_groups.values())
 | |
|         group_to_state = yield self._get_state_for_groups(groups, types)
 | |
| 
 | |
|         state_event_map = yield self.get_events(
 | |
|             [ev_id for sd in group_to_state.values() for ev_id in sd.values()],
 | |
|             get_prev_content=False
 | |
|         )
 | |
| 
 | |
|         event_to_state = {
 | |
|             event_id: {
 | |
|                 k: state_event_map[v]
 | |
|                 for k, v in group_to_state[group].items()
 | |
|                 if v in state_event_map
 | |
|             }
 | |
|             for event_id, group in event_to_groups.items()
 | |
|         }
 | |
| 
 | |
|         defer.returnValue({event: event_to_state[event] for event in event_ids})
 | |
| 
 | |
|     @defer.inlineCallbacks
 | |
|     def get_state_ids_for_events(self, event_ids, types):
 | |
|         event_to_groups = yield self._get_state_group_for_events(
 | |
|             event_ids,
 | |
|         )
 | |
| 
 | |
|         groups = set(event_to_groups.values())
 | |
|         group_to_state = yield self._get_state_for_groups(groups, types)
 | |
| 
 | |
|         event_to_state = {
 | |
|             event_id: group_to_state[group]
 | |
|             for event_id, group in event_to_groups.items()
 | |
|         }
 | |
| 
 | |
|         defer.returnValue({event: event_to_state[event] for event in event_ids})
 | |
| 
 | |
|     @defer.inlineCallbacks
 | |
|     def get_state_for_event(self, event_id, types=None):
 | |
|         """
 | |
|         Get the state dict corresponding to a particular event
 | |
| 
 | |
|         Args:
 | |
|             event_id(str): event whose state should be returned
 | |
|             types(list[(str, str)]|None): List of (type, state_key) tuples
 | |
|                 which are used to filter the state fetched. May be None, which
 | |
|                 matches any key
 | |
| 
 | |
|         Returns:
 | |
|             A deferred dict from (type, state_key) -> state_event
 | |
|         """
 | |
|         state_map = yield self.get_state_for_events([event_id], types)
 | |
|         defer.returnValue(state_map[event_id])
 | |
| 
 | |
|     @defer.inlineCallbacks
 | |
|     def get_state_ids_for_event(self, event_id, types=None):
 | |
|         """
 | |
|         Get the state dict corresponding to a particular event
 | |
| 
 | |
|         Args:
 | |
|             event_id(str): event whose state should be returned
 | |
|             types(list[(str, str)]|None): List of (type, state_key) tuples
 | |
|                 which are used to filter the state fetched. May be None, which
 | |
|                 matches any key
 | |
| 
 | |
|         Returns:
 | |
|             A deferred dict from (type, state_key) -> state_event
 | |
|         """
 | |
|         state_map = yield self.get_state_ids_for_events([event_id], types)
 | |
|         defer.returnValue(state_map[event_id])
 | |
| 
 | |
|     @cached(num_args=2, max_entries=10000)
 | |
|     def _get_state_group_for_event(self, room_id, event_id):
 | |
|         return self._simple_select_one_onecol(
 | |
|             table="event_to_state_groups",
 | |
|             keyvalues={
 | |
|                 "event_id": event_id,
 | |
|             },
 | |
|             retcol="state_group",
 | |
|             allow_none=True,
 | |
|             desc="_get_state_group_for_event",
 | |
|         )
 | |
| 
 | |
|     @cachedList(cached_method_name="_get_state_group_for_event",
 | |
|                 list_name="event_ids", num_args=1, inlineCallbacks=True)
 | |
|     def _get_state_group_for_events(self, event_ids):
 | |
|         """Returns mapping event_id -> state_group
 | |
|         """
 | |
|         rows = yield self._simple_select_many_batch(
 | |
|             table="event_to_state_groups",
 | |
|             column="event_id",
 | |
|             iterable=event_ids,
 | |
|             keyvalues={},
 | |
|             retcols=("event_id", "state_group",),
 | |
|             desc="_get_state_group_for_events",
 | |
|         )
 | |
| 
 | |
|         defer.returnValue({row["event_id"]: row["state_group"] for row in rows})
 | |
| 
 | |
|     def _get_some_state_from_cache(self, group, types):
 | |
|         """Checks if group is in cache. See `_get_state_for_groups`
 | |
| 
 | |
|         Returns 3-tuple (`state_dict`, `missing_types`, `got_all`).
 | |
|         `missing_types` is the list of types that aren't in the cache for that
 | |
|         group. `got_all` is a bool indicating if we successfully retrieved all
 | |
|         requests state from the cache, if False we need to query the DB for the
 | |
|         missing state.
 | |
| 
 | |
|         Args:
 | |
|             group: The state group to lookup
 | |
|             types (list): List of 2-tuples of the form (`type`, `state_key`),
 | |
|                 where a `state_key` of `None` matches all state_keys for the
 | |
|                 `type`.
 | |
|         """
 | |
|         is_all, state_dict_ids = self._state_group_cache.get(group)
 | |
| 
 | |
|         type_to_key = {}
 | |
|         missing_types = set()
 | |
|         for typ, state_key in types:
 | |
|             if state_key is None:
 | |
|                 type_to_key[typ] = None
 | |
|                 missing_types.add((typ, state_key))
 | |
|             else:
 | |
|                 if type_to_key.get(typ, object()) is not None:
 | |
|                     type_to_key.setdefault(typ, set()).add(state_key)
 | |
| 
 | |
|                 if (typ, state_key) not in state_dict_ids:
 | |
|                     missing_types.add((typ, state_key))
 | |
| 
 | |
|         sentinel = object()
 | |
| 
 | |
|         def include(typ, state_key):
 | |
|             valid_state_keys = type_to_key.get(typ, sentinel)
 | |
|             if valid_state_keys is sentinel:
 | |
|                 return False
 | |
|             if valid_state_keys is None:
 | |
|                 return True
 | |
|             if state_key in valid_state_keys:
 | |
|                 return True
 | |
|             return False
 | |
| 
 | |
|         got_all = not (missing_types or types is None)
 | |
| 
 | |
|         return {
 | |
|             k: v for k, v in state_dict_ids.items()
 | |
|             if include(k[0], k[1])
 | |
|         }, missing_types, got_all
 | |
| 
 | |
|     def _get_all_state_from_cache(self, group):
 | |
|         """Checks if group is in cache. See `_get_state_for_groups`
 | |
| 
 | |
|         Returns 2-tuple (`state_dict`, `got_all`). `got_all` is a bool
 | |
|         indicating if we successfully retrieved all requests state from the
 | |
|         cache, if False we need to query the DB for the missing state.
 | |
| 
 | |
|         Args:
 | |
|             group: The state group to lookup
 | |
|         """
 | |
|         is_all, state_dict_ids = self._state_group_cache.get(group)
 | |
| 
 | |
|         return state_dict_ids, is_all
 | |
| 
 | |
|     @defer.inlineCallbacks
 | |
|     def _get_state_for_groups(self, groups, types=None):
 | |
|         """Given list of groups returns dict of group -> list of state events
 | |
|         with matching types. `types` is a list of `(type, state_key)`, where
 | |
|         a `state_key` of None matches all state_keys. If `types` is None then
 | |
|         all events are returned.
 | |
|         """
 | |
|         if types:
 | |
|             types = frozenset(types)
 | |
|         results = {}
 | |
|         missing_groups = []
 | |
|         if types is not None:
 | |
|             for group in set(groups):
 | |
|                 state_dict_ids, missing_types, got_all = self._get_some_state_from_cache(
 | |
|                     group, types
 | |
|                 )
 | |
|                 results[group] = state_dict_ids
 | |
| 
 | |
|                 if not got_all:
 | |
|                     missing_groups.append(group)
 | |
|         else:
 | |
|             for group in set(groups):
 | |
|                 state_dict_ids, got_all = self._get_all_state_from_cache(
 | |
|                     group
 | |
|                 )
 | |
| 
 | |
|                 results[group] = state_dict_ids
 | |
| 
 | |
|                 if not got_all:
 | |
|                     missing_groups.append(group)
 | |
| 
 | |
|         if missing_groups:
 | |
|             # Okay, so we have some missing_types, lets fetch them.
 | |
|             cache_seq_num = self._state_group_cache.sequence
 | |
| 
 | |
|             group_to_state_dict = yield self._get_state_groups_from_groups(
 | |
|                 missing_groups, types
 | |
|             )
 | |
| 
 | |
|             # Now we want to update the cache with all the things we fetched
 | |
|             # from the database.
 | |
|             for group, group_state_dict in group_to_state_dict.items():
 | |
|                 if types:
 | |
|                     # We delibrately put key -> None mappings into the cache to
 | |
|                     # cache absence of the key, on the assumption that if we've
 | |
|                     # explicitly asked for some types then we will probably ask
 | |
|                     # for them again.
 | |
|                     state_dict = {
 | |
|                         (intern_string(etype), intern_string(state_key)): None
 | |
|                         for (etype, state_key) in types
 | |
|                     }
 | |
|                     state_dict.update(results[group])
 | |
|                     results[group] = state_dict
 | |
|                 else:
 | |
|                     state_dict = results[group]
 | |
| 
 | |
|                 state_dict.update(group_state_dict)
 | |
| 
 | |
|                 self._state_group_cache.update(
 | |
|                     cache_seq_num,
 | |
|                     key=group,
 | |
|                     value=state_dict,
 | |
|                     full=(types is None),
 | |
|                 )
 | |
| 
 | |
|         # Remove all the entries with None values. The None values were just
 | |
|         # used for bookkeeping in the cache.
 | |
|         for group, state_dict in results.items():
 | |
|             results[group] = {
 | |
|                 key: event_id
 | |
|                 for key, event_id in state_dict.items()
 | |
|                 if event_id
 | |
|             }
 | |
| 
 | |
|         defer.returnValue(results)
 | |
| 
 | |
|     def get_next_state_group(self):
 | |
|         return self._state_groups_id_gen.get_next()
 | |
| 
 | |
|     @defer.inlineCallbacks
 | |
|     def _background_deduplicate_state(self, progress, batch_size):
 | |
|         """This background update will slowly deduplicate state by reencoding
 | |
|         them as deltas.
 | |
|         """
 | |
|         last_state_group = progress.get("last_state_group", 0)
 | |
|         rows_inserted = progress.get("rows_inserted", 0)
 | |
|         max_group = progress.get("max_group", None)
 | |
| 
 | |
|         BATCH_SIZE_SCALE_FACTOR = 100
 | |
| 
 | |
|         batch_size = max(1, int(batch_size / BATCH_SIZE_SCALE_FACTOR))
 | |
| 
 | |
|         if max_group is None:
 | |
|             rows = yield self._execute(
 | |
|                 "_background_deduplicate_state", None,
 | |
|                 "SELECT coalesce(max(id), 0) FROM state_groups",
 | |
|             )
 | |
|             max_group = rows[0][0]
 | |
| 
 | |
|         def reindex_txn(txn):
 | |
|             new_last_state_group = last_state_group
 | |
|             for count in xrange(batch_size):
 | |
|                 txn.execute(
 | |
|                     "SELECT id, room_id FROM state_groups"
 | |
|                     " WHERE ? < id AND id <= ?"
 | |
|                     " ORDER BY id ASC"
 | |
|                     " LIMIT 1",
 | |
|                     (new_last_state_group, max_group,)
 | |
|                 )
 | |
|                 row = txn.fetchone()
 | |
|                 if row:
 | |
|                     state_group, room_id = row
 | |
| 
 | |
|                 if not row or not state_group:
 | |
|                     return True, count
 | |
| 
 | |
|                 txn.execute(
 | |
|                     "SELECT state_group FROM state_group_edges"
 | |
|                     " WHERE state_group = ?",
 | |
|                     (state_group,)
 | |
|                 )
 | |
| 
 | |
|                 # If we reach a point where we've already started inserting
 | |
|                 # edges we should stop.
 | |
|                 if txn.fetchall():
 | |
|                     return True, count
 | |
| 
 | |
|                 txn.execute(
 | |
|                     "SELECT coalesce(max(id), 0) FROM state_groups"
 | |
|                     " WHERE id < ? AND room_id = ?",
 | |
|                     (state_group, room_id,)
 | |
|                 )
 | |
|                 prev_group, = txn.fetchone()
 | |
|                 new_last_state_group = state_group
 | |
| 
 | |
|                 if prev_group:
 | |
|                     potential_hops = self._count_state_group_hops_txn(
 | |
|                         txn, prev_group
 | |
|                     )
 | |
|                     if potential_hops >= MAX_STATE_DELTA_HOPS:
 | |
|                         # We want to ensure chains are at most this long,#
 | |
|                         # otherwise read performance degrades.
 | |
|                         continue
 | |
| 
 | |
|                     prev_state = self._get_state_groups_from_groups_txn(
 | |
|                         txn, [prev_group], types=None
 | |
|                     )
 | |
|                     prev_state = prev_state[prev_group]
 | |
| 
 | |
|                     curr_state = self._get_state_groups_from_groups_txn(
 | |
|                         txn, [state_group], types=None
 | |
|                     )
 | |
|                     curr_state = curr_state[state_group]
 | |
| 
 | |
|                     if not set(prev_state.keys()) - set(curr_state.keys()):
 | |
|                         # We can only do a delta if the current has a strict super set
 | |
|                         # of keys
 | |
| 
 | |
|                         delta_state = {
 | |
|                             key: value for key, value in curr_state.items()
 | |
|                             if prev_state.get(key, None) != value
 | |
|                         }
 | |
| 
 | |
|                         self._simple_delete_txn(
 | |
|                             txn,
 | |
|                             table="state_group_edges",
 | |
|                             keyvalues={
 | |
|                                 "state_group": state_group,
 | |
|                             }
 | |
|                         )
 | |
| 
 | |
|                         self._simple_insert_txn(
 | |
|                             txn,
 | |
|                             table="state_group_edges",
 | |
|                             values={
 | |
|                                 "state_group": state_group,
 | |
|                                 "prev_state_group": prev_group,
 | |
|                             }
 | |
|                         )
 | |
| 
 | |
|                         self._simple_delete_txn(
 | |
|                             txn,
 | |
|                             table="state_groups_state",
 | |
|                             keyvalues={
 | |
|                                 "state_group": state_group,
 | |
|                             }
 | |
|                         )
 | |
| 
 | |
|                         self._simple_insert_many_txn(
 | |
|                             txn,
 | |
|                             table="state_groups_state",
 | |
|                             values=[
 | |
|                                 {
 | |
|                                     "state_group": state_group,
 | |
|                                     "room_id": room_id,
 | |
|                                     "type": key[0],
 | |
|                                     "state_key": key[1],
 | |
|                                     "event_id": state_id,
 | |
|                                 }
 | |
|                                 for key, state_id in delta_state.items()
 | |
|                             ],
 | |
|                         )
 | |
| 
 | |
|             progress = {
 | |
|                 "last_state_group": state_group,
 | |
|                 "rows_inserted": rows_inserted + batch_size,
 | |
|                 "max_group": max_group,
 | |
|             }
 | |
| 
 | |
|             self._background_update_progress_txn(
 | |
|                 txn, self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME, progress
 | |
|             )
 | |
| 
 | |
|             return False, batch_size
 | |
| 
 | |
|         finished, result = yield self.runInteraction(
 | |
|             self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME, reindex_txn
 | |
|         )
 | |
| 
 | |
|         if finished:
 | |
|             yield self._end_background_update(self.STATE_GROUP_DEDUPLICATION_UPDATE_NAME)
 | |
| 
 | |
|         defer.returnValue(result * BATCH_SIZE_SCALE_FACTOR)
 | |
| 
 | |
|     @defer.inlineCallbacks
 | |
|     def _background_index_state(self, progress, batch_size):
 | |
|         def reindex_txn(conn):
 | |
|             conn.rollback()
 | |
|             if isinstance(self.database_engine, PostgresEngine):
 | |
|                 # postgres insists on autocommit for the index
 | |
|                 conn.set_session(autocommit=True)
 | |
|                 try:
 | |
|                     txn = conn.cursor()
 | |
|                     txn.execute(
 | |
|                         "CREATE INDEX CONCURRENTLY state_groups_state_type_idx"
 | |
|                         " ON state_groups_state(state_group, type, state_key)"
 | |
|                     )
 | |
|                     txn.execute(
 | |
|                         "DROP INDEX IF EXISTS state_groups_state_id"
 | |
|                     )
 | |
|                 finally:
 | |
|                     conn.set_session(autocommit=False)
 | |
|             else:
 | |
|                 txn = conn.cursor()
 | |
|                 txn.execute(
 | |
|                     "CREATE INDEX state_groups_state_type_idx"
 | |
|                     " ON state_groups_state(state_group, type, state_key)"
 | |
|                 )
 | |
|                 txn.execute(
 | |
|                     "DROP INDEX IF EXISTS state_groups_state_id"
 | |
|                 )
 | |
| 
 | |
|         yield self.runWithConnection(reindex_txn)
 | |
| 
 | |
|         yield self._end_background_update(self.STATE_GROUP_INDEX_UPDATE_NAME)
 | |
| 
 | |
|         defer.returnValue(1)
 |