27
27
List ,
28
28
Mapping ,
29
29
Optional ,
30
+ Sequence ,
30
31
Tuple ,
31
32
TypeVar ,
32
33
Union ,
33
34
)
34
35
36
+ import attr
35
37
from prometheus_client import Counter
36
38
37
39
from twisted .internet import defer
@@ -455,6 +457,7 @@ async def _try_destination_list(
455
457
description : str ,
456
458
destinations : Iterable [str ],
457
459
callback : Callable [[str ], Awaitable [T ]],
460
+ failover_on_unknown_endpoint : bool = False ,
458
461
) -> T :
459
462
"""Try an operation on a series of servers, until it succeeds
460
463
@@ -474,6 +477,10 @@ async def _try_destination_list(
474
477
next server tried. Normally the stacktrace is logged but this is
475
478
suppressed if the exception is an InvalidResponseError.
476
479
480
+ failover_on_unknown_endpoint: if True, we will try other servers if it looks
481
+ like a server doesn't support the endpoint. This is typically useful
482
+ if the endpoint in question is new or experimental.
483
+
477
484
Returns:
478
485
The result of callback, if it succeeds
479
486
@@ -493,16 +500,31 @@ async def _try_destination_list(
493
500
except UnsupportedRoomVersionError :
494
501
raise
495
502
except HttpResponseException as e :
496
- if not 500 <= e .code < 600 :
497
- raise e .to_synapse_error ()
498
- else :
499
- logger .warning (
500
- "Failed to %s via %s: %i %s" ,
501
- description ,
502
- destination ,
503
- e .code ,
504
- e .args [0 ],
505
- )
503
+ synapse_error = e .to_synapse_error ()
504
+ failover = False
505
+
506
+ if 500 <= e .code < 600 :
507
+ failover = True
508
+
509
+ elif failover_on_unknown_endpoint :
510
+ # there is no good way to detect an "unknown" endpoint. Dendrite
511
+ # returns a 404 (with no body); synapse returns a 400
512
+ # with M_UNRECOGNISED.
513
+ if e .code == 404 or (
514
+ e .code == 400 and synapse_error .errcode == Codes .UNRECOGNIZED
515
+ ):
516
+ failover = True
517
+
518
+ if not failover :
519
+ raise synapse_error from e
520
+
521
+ logger .warning (
522
+ "Failed to %s via %s: %i %s" ,
523
+ description ,
524
+ destination ,
525
+ e .code ,
526
+ e .args [0 ],
527
+ )
506
528
except Exception :
507
529
logger .warning (
508
530
"Failed to %s via %s" , description , destination , exc_info = True
@@ -1042,3 +1064,141 @@ async def get_room_complexity(
1042
1064
# If we don't manage to find it, return None. It's not an error if a
1043
1065
# server doesn't give it to us.
1044
1066
return None
1067
+
1068
+ async def get_space_summary (
1069
+ self ,
1070
+ destinations : Iterable [str ],
1071
+ room_id : str ,
1072
+ suggested_only : bool ,
1073
+ max_rooms_per_space : Optional [int ],
1074
+ exclude_rooms : List [str ],
1075
+ ) -> "FederationSpaceSummaryResult" :
1076
+ """
1077
+ Call other servers to get a summary of the given space
1078
+
1079
+
1080
+ Args:
1081
+ destinations: The remote servers. We will try them in turn, omitting any
1082
+ that have been blacklisted.
1083
+
1084
+ room_id: ID of the space to be queried
1085
+
1086
+ suggested_only: If true, ask the remote server to only return children
1087
+ with the "suggested" flag set
1088
+
1089
+ max_rooms_per_space: A limit on the number of children to return for each
1090
+ space
1091
+
1092
+ exclude_rooms: A list of room IDs to tell the remote server to skip
1093
+
1094
+ Returns:
1095
+ a parsed FederationSpaceSummaryResult
1096
+
1097
+ Raises:
1098
+ SynapseError if we were unable to get a valid summary from any of the
1099
+ remote servers
1100
+ """
1101
+
1102
+ async def send_request (destination : str ) -> FederationSpaceSummaryResult :
1103
+ res = await self .transport_layer .get_space_summary (
1104
+ destination = destination ,
1105
+ room_id = room_id ,
1106
+ suggested_only = suggested_only ,
1107
+ max_rooms_per_space = max_rooms_per_space ,
1108
+ exclude_rooms = exclude_rooms ,
1109
+ )
1110
+
1111
+ try :
1112
+ return FederationSpaceSummaryResult .from_json_dict (res )
1113
+ except ValueError as e :
1114
+ raise InvalidResponseError (str (e ))
1115
+
1116
+ return await self ._try_destination_list (
1117
+ "fetch space summary" ,
1118
+ destinations ,
1119
+ send_request ,
1120
+ failover_on_unknown_endpoint = True ,
1121
+ )
1122
+
1123
+
1124
+ @attr .s (frozen = True , slots = True )
1125
+ class FederationSpaceSummaryEventResult :
1126
+ """Represents a single event in the result of a successful get_space_summary call.
1127
+
1128
+ It's essentially just a serialised event object, but we do a bit of parsing and
1129
+ validation in `from_json_dict` and store some of the validated properties in
1130
+ object attributes.
1131
+ """
1132
+
1133
+ event_type = attr .ib (type = str )
1134
+ state_key = attr .ib (type = str )
1135
+ via = attr .ib (type = Sequence [str ])
1136
+
1137
+ # the raw data, including the above keys
1138
+ data = attr .ib (type = JsonDict )
1139
+
1140
+ @classmethod
1141
+ def from_json_dict (cls , d : JsonDict ) -> "FederationSpaceSummaryEventResult" :
1142
+ """Parse an event within the result of a /spaces/ request
1143
+
1144
+ Args:
1145
+ d: json object to be parsed
1146
+
1147
+ Raises:
1148
+ ValueError if d is not a valid event
1149
+ """
1150
+
1151
+ event_type = d .get ("type" )
1152
+ if not isinstance (event_type , str ):
1153
+ raise ValueError ("Invalid event: 'event_type' must be a str" )
1154
+
1155
+ state_key = d .get ("state_key" )
1156
+ if not isinstance (state_key , str ):
1157
+ raise ValueError ("Invalid event: 'state_key' must be a str" )
1158
+
1159
+ content = d .get ("content" )
1160
+ if not isinstance (content , dict ):
1161
+ raise ValueError ("Invalid event: 'content' must be a dict" )
1162
+
1163
+ via = content .get ("via" )
1164
+ if not isinstance (via , Sequence ):
1165
+ raise ValueError ("Invalid event: 'via' must be a list" )
1166
+ if any (not isinstance (v , str ) for v in via ):
1167
+ raise ValueError ("Invalid event: 'via' must be a list of strings" )
1168
+
1169
+ return cls (event_type , state_key , via , d )
1170
+
1171
+
1172
+ @attr .s (frozen = True , slots = True )
1173
+ class FederationSpaceSummaryResult :
1174
+ """Represents the data returned by a successful get_space_summary call."""
1175
+
1176
+ rooms = attr .ib (type = Sequence [JsonDict ])
1177
+ events = attr .ib (type = Sequence [FederationSpaceSummaryEventResult ])
1178
+
1179
+ @classmethod
1180
+ def from_json_dict (cls , d : JsonDict ) -> "FederationSpaceSummaryResult" :
1181
+ """Parse the result of a /spaces/ request
1182
+
1183
+ Args:
1184
+ d: json object to be parsed
1185
+
1186
+ Raises:
1187
+ ValueError if d is not a valid /spaces/ response
1188
+ """
1189
+ rooms = d .get ("rooms" )
1190
+ if not isinstance (rooms , Sequence ):
1191
+ raise ValueError ("'rooms' must be a list" )
1192
+ if any (not isinstance (r , dict ) for r in rooms ):
1193
+ raise ValueError ("Invalid room in 'rooms' list" )
1194
+
1195
+ events = d .get ("events" )
1196
+ if not isinstance (events , Sequence ):
1197
+ raise ValueError ("'events' must be a list" )
1198
+ if any (not isinstance (e , dict ) for e in events ):
1199
+ raise ValueError ("Invalid event in 'events' list" )
1200
+ parsed_events = [
1201
+ FederationSpaceSummaryEventResult .from_json_dict (e ) for e in events
1202
+ ]
1203
+
1204
+ return cls (rooms , parsed_events )
0 commit comments