918 operation.handle_event('rollback_event') |
918 operation.handle_event('rollback_event') |
919 except BaseException: |
919 except BaseException: |
920 self.critical('rollback error', exc_info=sys.exc_info()) |
920 self.critical('rollback error', exc_info=sys.exc_info()) |
921 continue |
921 continue |
922 cnxset.rollback() |
922 cnxset.rollback() |
923 self.debug('rollback for connectionid %s done', self.connectionid) |
923 self.debug('rollback for transaction %s done', self.connectionid) |
924 finally: |
924 finally: |
925 self._session_timestamp.touch() |
925 self._session_timestamp.touch() |
926 if free_cnxset: |
926 if free_cnxset: |
927 self.free_cnxset(ignoremode=True) |
927 self.free_cnxset(ignoremode=True) |
928 self.clear() |
928 self.clear() |
929 |
|
930 # resource accessors ###################################################### |
|
931 |
|
932 def system_sql(self, sql, args=None, rollback_on_failure=True): |
|
933 """return a sql cursor on the system database""" |
|
934 if sql.split(None, 1)[0].upper() != 'SELECT': |
|
935 self.mode = 'write' |
|
936 source = self.cnxset.source('system') |
|
937 try: |
|
938 return source.doexec(self, sql, args, rollback=rollback_on_failure) |
|
939 except (source.OperationalError, source.InterfaceError): |
|
940 if not rollback_on_failure: |
|
941 raise |
|
942 source.warning("trying to reconnect") |
|
943 self.cnxset.reconnect(source) |
|
944 return source.doexec(self, sql, args, rollback=rollback_on_failure) |
|
945 |
|
946 def rtype_eids_rdef(self, rtype, eidfrom, eidto): |
|
947 # use type_and_source_from_eid instead of type_from_eid for optimization |
|
948 # (avoid two extra methods call) |
|
949 subjtype = self.repo.type_and_source_from_eid(eidfrom, self)[0] |
|
950 objtype = self.repo.type_and_source_from_eid(eidto, self)[0] |
|
951 return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)] |
|
952 |
|
953 |
|
954 def cnx_attr(attr_name, writable=False): |
|
955 """return a property to forward attribute access to connection. |
|
956 |
|
957 This is to be used by session""" |
|
958 args = {} |
|
959 def attr_from_cnx(session): |
|
960 return getattr(session._cnx, attr_name) |
|
961 args['fget'] = attr_from_cnx |
|
962 if writable: |
|
963 def write_attr(session, value): |
|
964 return setattr(session._cnx, attr_name, value) |
|
965 args['fset'] = write_attr |
|
966 return property(**args) |
|
967 |
|
968 def cnx_meth(meth_name): |
|
969 """return a function forwarding calls to connection. |
|
970 |
|
971 This is to be used by session""" |
|
972 def meth_from_cnx(session, *args, **kwargs): |
|
973 result = getattr(session._cnx, meth_name)(*args, **kwargs) |
|
974 if getattr(result, '_cw', None) is not None: |
|
975 result._cw = session |
|
976 return result |
|
977 return meth_from_cnx |
|
978 |
|
979 class Timestamp(object): |
|
980 |
|
981 def __init__(self): |
|
982 self.value = time() |
|
983 |
|
984 def touch(self): |
|
985 self.value = time() |
|
986 |
|
987 def __float__(self): |
|
988 return float(self.value) |
|
989 |
|
990 |
|
991 class Session(RequestSessionBase): |
|
992 """Repository user session |
|
993 |
|
994 This tie all together: |
|
995 * session id, |
|
996 * user, |
|
997 * connections set, |
|
998 * other session data. |
|
999 |
|
1000 About session storage / transactions |
|
1001 ------------------------------------ |
|
1002 |
|
1003 Here is a description of internal session attributes. Besides :attr:`data` |
|
1004 and :attr:`transaction_data`, you should not have to use attributes |
|
1005 described here but higher level APIs. |
|
1006 |
|
1007 :attr:`data` is a dictionary containing shared data, used to communicate |
|
1008 extra information between the client and the repository |
|
1009 |
|
1010 :attr:`_cnxs` is a dictionary of :class:`Connection` instance, one |
|
1011 for each running connection. The key is the connection id. By default |
|
1012 the connection id is the thread name but it can be otherwise (per dbapi |
|
1013 cursor for instance, or per thread name *from another process*). |
|
1014 |
|
1015 :attr:`__threaddata` is a thread local storage whose `cnx` attribute |
|
1016 refers to the proper instance of :class:`Connection` according to the |
|
1017 connection. |
|
1018 |
|
1019 You should not have to use neither :attr:`_cnx` nor :attr:`__threaddata`, |
|
1020 simply access connection data transparently through the :attr:`_cnx` |
|
1021 property. Also, you usually don't have to access it directly since current |
|
1022 connection's data may be accessed/modified through properties / methods: |
|
1023 |
|
1024 :attr:`connection_data`, similarly to :attr:`data`, is a dictionary |
|
1025 containing some shared data that should be cleared at the end of the |
|
1026 connection. Hooks and operations may put arbitrary data in there, and |
|
1027 this may also be used as a communication channel between the client and |
|
1028 the repository. |
|
1029 |
|
1030 .. automethod:: cubicweb.server.session.Session.get_shared_data |
|
1031 .. automethod:: cubicweb.server.session.Session.set_shared_data |
|
1032 .. automethod:: cubicweb.server.session.Session.added_in_transaction |
|
1033 .. automethod:: cubicweb.server.session.Session.deleted_in_transaction |
|
1034 |
|
1035 Connection state information: |
|
1036 |
|
1037 :attr:`running_dbapi_query`, boolean flag telling if the executing query |
|
1038 is coming from a dbapi connection or is a query from within the repository |
|
1039 |
|
1040 :attr:`cnxset`, the connections set to use to execute queries on sources. |
|
1041 During a transaction, the connection set may be freed so that is may be |
|
1042 used by another session as long as no writing is done. This means we can |
|
1043 have multiple sessions with a reasonably low connections set pool size. |
|
1044 |
|
1045 .. automethod:: cubicweb.server.session.set_cnxset |
|
1046 .. automethod:: cubicweb.server.session.free_cnxset |
|
1047 |
|
1048 :attr:`mode`, string telling the connections set handling mode, may be one |
|
1049 of 'read' (connections set may be freed), 'write' (some write was done in |
|
1050 the connections set, it can't be freed before end of the transaction), |
|
1051 'transaction' (we want to keep the connections set during all the |
|
1052 transaction, with or without writing) |
|
1053 |
|
1054 :attr:`pending_operations`, ordered list of operations to be processed on |
|
1055 commit/rollback |
|
1056 |
|
1057 :attr:`commit_state`, describing the transaction commit state, may be one |
|
1058 of None (not yet committing), 'precommit' (calling precommit event on |
|
1059 operations), 'postcommit' (calling postcommit event on operations), |
|
1060 'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error |
|
1061 has been raised during the transaction and so it must be rollbacked). |
|
1062 |
|
1063 .. automethod:: cubicweb.server.session.Session.commit |
|
1064 .. automethod:: cubicweb.server.session.Session.rollback |
|
1065 .. automethod:: cubicweb.server.session.Session.close |
|
1066 .. automethod:: cubicweb.server.session.Session.closed |
|
1067 |
|
1068 Security level Management: |
|
1069 |
|
1070 :attr:`read_security` and :attr:`write_security`, boolean flags telling if |
|
1071 read/write security is currently activated. |
|
1072 |
|
1073 .. automethod:: cubicweb.server.session.Session.security_enabled |
|
1074 |
|
1075 Hooks Management: |
|
1076 |
|
1077 :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`. |
|
1078 |
|
1079 :attr:`enabled_hook_categories`, when :attr:`hooks_mode` is |
|
1080 `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled. |
|
1081 |
|
1082 :attr:`disabled_hook_categories`, when :attr:`hooks_mode` is |
|
1083 `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled. |
|
1084 |
|
1085 .. automethod:: cubicweb.server.session.Session.deny_all_hooks_but |
|
1086 .. automethod:: cubicweb.server.session.Session.allow_all_hooks_but |
|
1087 .. automethod:: cubicweb.server.session.Session.is_hook_category_activated |
|
1088 .. automethod:: cubicweb.server.session.Session.is_hook_activated |
|
1089 |
|
1090 Data manipulation: |
|
1091 |
|
1092 .. automethod:: cubicweb.server.session.Session.add_relation |
|
1093 .. automethod:: cubicweb.server.session.Session.add_relations |
|
1094 .. automethod:: cubicweb.server.session.Session.delete_relation |
|
1095 |
|
1096 Other: |
|
1097 |
|
1098 .. automethod:: cubicweb.server.session.Session.call_service |
|
1099 |
|
1100 |
|
1101 |
|
1102 """ |
|
1103 is_request = False |
|
1104 is_internal_session = False |
|
1105 |
|
1106 def __init__(self, user, repo, cnxprops=None, _id=None): |
|
1107 super(Session, self).__init__(repo.vreg) |
|
1108 self.id = _id or make_uid(unormalize(user.login).encode('UTF8')) |
|
1109 self.user = user |
|
1110 self.repo = repo |
|
1111 self._timestamp = Timestamp() |
|
1112 self.default_mode = 'read' |
|
1113 # short cut to querier .execute method |
|
1114 self._execute = repo.querier.execute |
|
1115 # shared data, used to communicate extra information between the client |
|
1116 # and the rql server |
|
1117 self.data = {} |
|
1118 # i18n initialization |
|
1119 self.set_language(user.prefered_language()) |
|
1120 ### internals |
|
1121 # Connection of this section |
|
1122 self._cnxs = {} |
|
1123 # Data local to the thread |
|
1124 self.__threaddata = threading.local() |
|
1125 self._cnxset_tracker = CnxSetTracker() |
|
1126 self._closed = False |
|
1127 self._lock = threading.RLock() |
|
1128 |
|
1129 def __unicode__(self): |
|
1130 return '<session %s (%s 0x%x)>' % ( |
|
1131 unicode(self.user.login), self.id, id(self)) |
|
1132 @property |
|
1133 def timestamp(self): |
|
1134 return float(self._timestamp) |
|
1135 |
|
1136 @property |
|
1137 def sessionid(self): |
|
1138 return self.id |
|
1139 |
|
1140 @property |
|
1141 def login(self): |
|
1142 # XXX backward compat with dbapi. deprecate me ASAP. |
|
1143 return self.user.login |
|
1144 |
|
1145 def get_cnx(self, cnxid): |
|
1146 """return the <cnxid> connection attached to this session |
|
1147 |
|
1148 Connection is created if necessary""" |
|
1149 with self._lock: # no connection exist with the same id |
|
1150 try: |
|
1151 if self.closed: |
|
1152 raise SessionClosedError('try to access connections set on a closed session %s' % self.id) |
|
1153 cnx = self._cnxs[cnxid] |
|
1154 except KeyError: |
|
1155 cnx = Connection(cnxid, self) |
|
1156 self._cnxs[cnxid] = cnx |
|
1157 return cnx |
|
1158 |
|
1159 def set_cnx(self, cnxid=None): |
|
1160 """set the default connection of the current thread to <cnxid> |
|
1161 |
|
1162 Connection is created if necessary""" |
|
1163 if cnxid is None: |
|
1164 cnxid = threading.currentThread().getName() |
|
1165 self.__threaddata.cnx = self.get_cnx(cnxid) |
|
1166 |
|
1167 @property |
|
1168 def _cnx(self): |
|
1169 """default connection for current session in current thread""" |
|
1170 try: |
|
1171 return self.__threaddata.cnx |
|
1172 except AttributeError: |
|
1173 self.set_cnx() |
|
1174 return self.__threaddata.cnx |
|
1175 |
|
1176 @property |
|
1177 def _current_cnx_id(self): |
|
1178 """TRANSITIONAL PURPOSE""" |
|
1179 try: |
|
1180 return self.__threaddata.cnx.transactionid |
|
1181 except AttributeError: |
|
1182 return None |
|
1183 |
|
1184 def get_option_value(self, option, foreid=None): |
|
1185 return self.repo.get_option_value(option, foreid) |
|
1186 |
|
1187 def transaction(self, free_cnxset=True): |
|
1188 """return context manager to enter a transaction for the session: when |
|
1189 exiting the `with` block on exception, call `session.rollback()`, else |
|
1190 call `session.commit()` on normal exit. |
|
1191 |
|
1192 The `free_cnxset` will be given to rollback/commit methods to indicate |
|
1193 wether the connections set should be freed or not. |
|
1194 """ |
|
1195 return transaction(self, free_cnxset) |
|
1196 |
|
1197 add_relation = cnx_meth('add_relation') |
|
1198 add_relations = cnx_meth('add_relations') |
|
1199 delete_relation = cnx_meth('delete_relation') |
|
1200 |
|
1201 # relations cache handling ################################################# |
|
1202 |
|
1203 update_rel_cache_add = cnx_meth('update_rel_cache_add') |
|
1204 update_rel_cache_del = cnx_meth('update_rel_cache_del') |
|
1205 |
|
1206 # resource accessors ###################################################### |
|
1207 |
|
1208 system_sql = cnx_meth('system_sql') |
|
1209 deleted_in_transaction = cnx_meth('deleted_in_transaction') |
|
1210 added_in_transaction = cnx_meth('added_in_transaction') |
|
1211 rtype_eids_rdef = cnx_meth('rtype_eids_rdef') |
|
1212 |
|
1213 # security control ######################################################### |
|
1214 |
|
1215 |
|
1216 def security_enabled(self, read=None, write=None): |
|
1217 return _session_security_enabled(self, read=read, write=write) |
|
1218 |
|
1219 read_security = cnx_attr('read_security', writable=True) |
|
1220 write_security = cnx_attr('write_security', writable=True) |
|
1221 running_dbapi_query = cnx_attr('running_dbapi_query') |
|
1222 |
|
1223 # hooks activation control ################################################# |
|
1224 # all hooks should be activated during normal execution |
|
1225 |
|
1226 def allow_all_hooks_but(self, *categories): |
|
1227 return _session_hooks_control(self, HOOKS_ALLOW_ALL, *categories) |
|
1228 def deny_all_hooks_but(self, *categories): |
|
1229 return _session_hooks_control(self, HOOKS_DENY_ALL, *categories) |
|
1230 |
|
1231 hooks_mode = cnx_attr('hooks_mode') |
|
1232 |
|
1233 disabled_hook_categories = cnx_attr('disabled_hook_cats') |
|
1234 enabled_hook_categories = cnx_attr('enabled_hook_cats') |
|
1235 disable_hook_categories = cnx_meth('disable_hook_categories') |
|
1236 enable_hook_categories = cnx_meth('enable_hook_categories') |
|
1237 is_hook_category_activated = cnx_meth('is_hook_category_activated') |
|
1238 is_hook_activated = cnx_meth('is_hook_activated') |
|
1239 |
|
1240 # connection management ################################################### |
|
1241 |
|
1242 def keep_cnxset_mode(self, mode): |
|
1243 """set `mode`, e.g. how the session will keep its connections set: |
|
1244 |
|
1245 * if mode == 'write', the connections set is freed after each ready |
|
1246 query, but kept until the transaction's end (eg commit or rollback) |
|
1247 when a write query is detected (eg INSERT/SET/DELETE queries) |
|
1248 |
|
1249 * if mode == 'transaction', the connections set is only freed after the |
|
1250 transaction's end |
|
1251 |
|
1252 notice that a repository has a limited set of connections sets, and a |
|
1253 session has to wait for a free connections set to run any rql query |
|
1254 (unless it already has one set). |
|
1255 """ |
|
1256 assert mode in ('transaction', 'write') |
|
1257 if mode == 'transaction': |
|
1258 self.default_mode = 'transaction' |
|
1259 else: # mode == 'write' |
|
1260 self.default_mode = 'read' |
|
1261 |
|
1262 mode = cnx_attr('mode', writable=True) |
|
1263 commit_state = cnx_attr('commit_state', writable=True) |
|
1264 |
|
1265 @property |
|
1266 def cnxset(self): |
|
1267 """connections set, set according to transaction mode for each query""" |
|
1268 if self._closed: |
|
1269 self.free_cnxset(True) |
|
1270 raise SessionClosedError('try to access connections set on a closed session %s' % self.id) |
|
1271 return self._cnx.cnxset |
|
1272 |
|
1273 def set_cnxset(self): |
|
1274 """the session need a connections set to execute some queries""" |
|
1275 with self._lock: # can probably be removed |
|
1276 if self._closed: |
|
1277 self.free_cnxset(True) |
|
1278 raise SessionClosedError('try to set connections set on a closed session %s' % self.id) |
|
1279 return self._cnx.set_cnxset() |
|
1280 free_cnxset = cnx_meth('free_cnxset') |
|
1281 |
|
1282 def _touch(self): |
|
1283 """update latest session usage timestamp and reset mode to read""" |
|
1284 self._timestamp.touch() |
|
1285 |
|
1286 local_perm_cache = cnx_attr('local_perm_cache') |
|
1287 @local_perm_cache.setter |
|
1288 def local_perm_cache(self, value): |
|
1289 #base class assign an empty dict:-( |
|
1290 assert value == {} |
|
1291 pass |
|
1292 |
|
1293 # shared data handling ################################################### |
|
1294 |
|
1295 def get_shared_data(self, key, default=None, pop=False, txdata=False): |
|
1296 """return value associated to `key` in session data""" |
|
1297 if txdata: |
|
1298 return self._cnx.get_shared_data(key, default, pop, txdata=True) |
|
1299 else: |
|
1300 data = self.data |
|
1301 if pop: |
|
1302 return data.pop(key, default) |
|
1303 else: |
|
1304 return data.get(key, default) |
|
1305 |
|
1306 def set_shared_data(self, key, value, txdata=False): |
|
1307 """set value associated to `key` in session data""" |
|
1308 if txdata: |
|
1309 return self._cnx.set_shared_data(key, value, txdata=True) |
|
1310 else: |
|
1311 self.data[key] = value |
|
1312 |
|
1313 # server-side service call ################################################# |
|
1314 |
|
1315 def call_service(self, regid, **kwargs): |
|
1316 return self.repo._call_service_with_session(self, regid, |
|
1317 **kwargs) |
|
1318 |
|
1319 # request interface ####################################################### |
|
1320 |
|
1321 @property |
|
1322 def cursor(self): |
|
1323 """return a rql cursor""" |
|
1324 return self |
|
1325 |
|
1326 set_entity_cache = cnx_meth('set_entity_cache') |
|
1327 entity_cache = cnx_meth('entity_cache') |
|
1328 cache_entities = cnx_meth('cached_entities') |
|
1329 drop_entity_cache = cnx_meth('drop_entity_cache') |
|
1330 |
|
1331 source_defs = cnx_meth('source_defs') |
|
1332 describe = cnx_meth('describe') |
|
1333 source_from_eid = cnx_meth('source_from_eid') |
|
1334 |
|
1335 |
|
1336 def execute(self, *args, **kwargs): |
|
1337 """db-api like method directly linked to the querier execute method. |
|
1338 |
|
1339 See :meth:`cubicweb.dbapi.Cursor.execute` documentation. |
|
1340 """ |
|
1341 rset = self._cnx.execute(*args, **kwargs) |
|
1342 rset.req = self |
|
1343 return rset |
|
1344 |
|
1345 def close_cnx(self, cnxid): |
|
1346 cnx = self._cnxs.get(cnxid, None) |
|
1347 if cnx is not None: |
|
1348 cnx.free_cnxset(ignoremode=True) |
|
1349 self._clear_thread_storage(cnx) |
|
1350 self._clear_cnx_storage(cnx) |
|
1351 |
|
1352 |
|
1353 def _clear_thread_data(self, free_cnxset=True): |
|
1354 """remove everything from the thread local storage, except connections set |
|
1355 which is explicitly removed by free_cnxset, and mode which is set anyway |
|
1356 by _touch |
|
1357 """ |
|
1358 try: |
|
1359 cnx = self.__threaddata.cnx |
|
1360 except AttributeError: |
|
1361 pass |
|
1362 else: |
|
1363 if free_cnxset: |
|
1364 self.free_cnxset() |
|
1365 if cnx.ctx_count == 0: |
|
1366 self._clear_thread_storage(cnx) |
|
1367 else: |
|
1368 self._clear_cnx_storage(cnx) |
|
1369 else: |
|
1370 self._clear_cnx_storage(cnx) |
|
1371 |
|
1372 def _clear_thread_storage(self, cnx): |
|
1373 self._cnxs.pop(cnx.connectionid, None) |
|
1374 try: |
|
1375 if self.__threaddata.cnx is cnx: |
|
1376 del self.__threaddata.cnx |
|
1377 except AttributeError: |
|
1378 pass |
|
1379 |
|
1380 def _clear_cnx_storage(self, cnx): |
|
1381 cnx.clear() |
|
1382 |
929 |
1383 def commit(self, free_cnxset=True, reset_pool=None): |
930 def commit(self, free_cnxset=True, reset_pool=None): |
1384 """commit the current session's transaction""" |
931 """commit the current session's transaction""" |
1385 if reset_pool is not None: |
932 if reset_pool is not None: |
1386 warn('[3.13] use free_cnxset argument instead for reset_pool', |
933 warn('[3.13] use free_cnxset argument instead for reset_pool', |
1387 DeprecationWarning, stacklevel=2) |
934 DeprecationWarning, stacklevel=2) |
1388 free_cnxset = reset_pool |
935 free_cnxset = reset_pool |
1389 if self.cnxset is None: |
936 if self.cnxset is None: |
1390 assert not self.pending_operations |
937 assert not self.pending_operations |
1391 self._clear_thread_data() |
938 self.clear() |
1392 self._touch() |
939 self._session_timestamp.touch() |
1393 self.debug('commit session %s done (no db activity)', self.id) |
940 self.debug('commit transaction %s done (no db activity)', self.connectionid) |
1394 return |
941 return |
1395 cstate = self.commit_state |
942 cstate = self.commit_state |
1396 if cstate == 'uncommitable': |
943 if cstate == 'uncommitable': |
1397 raise QueryError('transaction must be rollbacked') |
944 raise QueryError('transaction must be rollbacked') |
1398 if cstate is not None: |
945 if cstate is not None: |
1458 try: |
1005 try: |
1459 operation.handle_event('postcommit_event') |
1006 operation.handle_event('postcommit_event') |
1460 except BaseException: |
1007 except BaseException: |
1461 self.critical('error while postcommit', |
1008 self.critical('error while postcommit', |
1462 exc_info=sys.exc_info()) |
1009 exc_info=sys.exc_info()) |
1463 self.debug('postcommit session %s done', self.id) |
1010 self.debug('postcommit transaction %s done', self.connectionid) |
1464 return self.transaction_uuid(set=False) |
1011 return self.transaction_uuid(set=False) |
1465 finally: |
1012 finally: |
1466 self._touch() |
1013 self._session_timestamp.touch() |
1467 if free_cnxset: |
1014 if free_cnxset: |
1468 self.free_cnxset(ignoremode=True) |
1015 self.free_cnxset(ignoremode=True) |
|
1016 self.clear() |
|
1017 |
|
1018 # resource accessors ###################################################### |
|
1019 |
|
1020 def system_sql(self, sql, args=None, rollback_on_failure=True): |
|
1021 """return a sql cursor on the system database""" |
|
1022 if sql.split(None, 1)[0].upper() != 'SELECT': |
|
1023 self.mode = 'write' |
|
1024 source = self.cnxset.source('system') |
|
1025 try: |
|
1026 return source.doexec(self, sql, args, rollback=rollback_on_failure) |
|
1027 except (source.OperationalError, source.InterfaceError): |
|
1028 if not rollback_on_failure: |
|
1029 raise |
|
1030 source.warning("trying to reconnect") |
|
1031 self.cnxset.reconnect(source) |
|
1032 return source.doexec(self, sql, args, rollback=rollback_on_failure) |
|
1033 |
|
1034 def rtype_eids_rdef(self, rtype, eidfrom, eidto): |
|
1035 # use type_and_source_from_eid instead of type_from_eid for optimization |
|
1036 # (avoid two extra methods call) |
|
1037 subjtype = self.repo.type_and_source_from_eid(eidfrom, self)[0] |
|
1038 objtype = self.repo.type_and_source_from_eid(eidto, self)[0] |
|
1039 return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)] |
|
1040 |
|
1041 |
|
1042 def cnx_attr(attr_name, writable=False): |
|
1043 """return a property to forward attribute access to connection. |
|
1044 |
|
1045 This is to be used by session""" |
|
1046 args = {} |
|
1047 def attr_from_cnx(session): |
|
1048 return getattr(session._cnx, attr_name) |
|
1049 args['fget'] = attr_from_cnx |
|
1050 if writable: |
|
1051 def write_attr(session, value): |
|
1052 return setattr(session._cnx, attr_name, value) |
|
1053 args['fset'] = write_attr |
|
1054 return property(**args) |
|
1055 |
|
1056 def cnx_meth(meth_name): |
|
1057 """return a function forwarding calls to connection. |
|
1058 |
|
1059 This is to be used by session""" |
|
1060 def meth_from_cnx(session, *args, **kwargs): |
|
1061 result = getattr(session._cnx, meth_name)(*args, **kwargs) |
|
1062 if getattr(result, '_cw', None) is not None: |
|
1063 result._cw = session |
|
1064 return result |
|
1065 return meth_from_cnx |
|
1066 |
|
1067 class Timestamp(object): |
|
1068 |
|
1069 def __init__(self): |
|
1070 self.value = time() |
|
1071 |
|
1072 def touch(self): |
|
1073 self.value = time() |
|
1074 |
|
1075 def __float__(self): |
|
1076 return float(self.value) |
|
1077 |
|
1078 |
|
1079 class Session(RequestSessionBase): |
|
1080 """Repository user session |
|
1081 |
|
1082 This tie all together: |
|
1083 * session id, |
|
1084 * user, |
|
1085 * connections set, |
|
1086 * other session data. |
|
1087 |
|
1088 About session storage / transactions |
|
1089 ------------------------------------ |
|
1090 |
|
1091 Here is a description of internal session attributes. Besides :attr:`data` |
|
1092 and :attr:`transaction_data`, you should not have to use attributes |
|
1093 described here but higher level APIs. |
|
1094 |
|
1095 :attr:`data` is a dictionary containing shared data, used to communicate |
|
1096 extra information between the client and the repository |
|
1097 |
|
1098 :attr:`_cnxs` is a dictionary of :class:`Connection` instance, one |
|
1099 for each running connection. The key is the connection id. By default |
|
1100 the connection id is the thread name but it can be otherwise (per dbapi |
|
1101 cursor for instance, or per thread name *from another process*). |
|
1102 |
|
1103 :attr:`__threaddata` is a thread local storage whose `cnx` attribute |
|
1104 refers to the proper instance of :class:`Connection` according to the |
|
1105 connection. |
|
1106 |
|
1107 You should not have to use neither :attr:`_cnx` nor :attr:`__threaddata`, |
|
1108 simply access connection data transparently through the :attr:`_cnx` |
|
1109 property. Also, you usually don't have to access it directly since current |
|
1110 connection's data may be accessed/modified through properties / methods: |
|
1111 |
|
1112 :attr:`connection_data`, similarly to :attr:`data`, is a dictionary |
|
1113 containing some shared data that should be cleared at the end of the |
|
1114 connection. Hooks and operations may put arbitrary data in there, and |
|
1115 this may also be used as a communication channel between the client and |
|
1116 the repository. |
|
1117 |
|
1118 .. automethod:: cubicweb.server.session.Session.get_shared_data |
|
1119 .. automethod:: cubicweb.server.session.Session.set_shared_data |
|
1120 .. automethod:: cubicweb.server.session.Session.added_in_transaction |
|
1121 .. automethod:: cubicweb.server.session.Session.deleted_in_transaction |
|
1122 |
|
1123 Connection state information: |
|
1124 |
|
1125 :attr:`running_dbapi_query`, boolean flag telling if the executing query |
|
1126 is coming from a dbapi connection or is a query from within the repository |
|
1127 |
|
1128 :attr:`cnxset`, the connections set to use to execute queries on sources. |
|
1129 During a transaction, the connection set may be freed so that is may be |
|
1130 used by another session as long as no writing is done. This means we can |
|
1131 have multiple sessions with a reasonably low connections set pool size. |
|
1132 |
|
1133 .. automethod:: cubicweb.server.session.set_cnxset |
|
1134 .. automethod:: cubicweb.server.session.free_cnxset |
|
1135 |
|
1136 :attr:`mode`, string telling the connections set handling mode, may be one |
|
1137 of 'read' (connections set may be freed), 'write' (some write was done in |
|
1138 the connections set, it can't be freed before end of the transaction), |
|
1139 'transaction' (we want to keep the connections set during all the |
|
1140 transaction, with or without writing) |
|
1141 |
|
1142 :attr:`pending_operations`, ordered list of operations to be processed on |
|
1143 commit/rollback |
|
1144 |
|
1145 :attr:`commit_state`, describing the transaction commit state, may be one |
|
1146 of None (not yet committing), 'precommit' (calling precommit event on |
|
1147 operations), 'postcommit' (calling postcommit event on operations), |
|
1148 'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error |
|
1149 has been raised during the transaction and so it must be rollbacked). |
|
1150 |
|
1151 .. automethod:: cubicweb.server.session.Session.commit |
|
1152 .. automethod:: cubicweb.server.session.Session.rollback |
|
1153 .. automethod:: cubicweb.server.session.Session.close |
|
1154 .. automethod:: cubicweb.server.session.Session.closed |
|
1155 |
|
1156 Security level Management: |
|
1157 |
|
1158 :attr:`read_security` and :attr:`write_security`, boolean flags telling if |
|
1159 read/write security is currently activated. |
|
1160 |
|
1161 .. automethod:: cubicweb.server.session.Session.security_enabled |
|
1162 |
|
1163 Hooks Management: |
|
1164 |
|
1165 :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`. |
|
1166 |
|
1167 :attr:`enabled_hook_categories`, when :attr:`hooks_mode` is |
|
1168 `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled. |
|
1169 |
|
1170 :attr:`disabled_hook_categories`, when :attr:`hooks_mode` is |
|
1171 `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled. |
|
1172 |
|
1173 .. automethod:: cubicweb.server.session.Session.deny_all_hooks_but |
|
1174 .. automethod:: cubicweb.server.session.Session.allow_all_hooks_but |
|
1175 .. automethod:: cubicweb.server.session.Session.is_hook_category_activated |
|
1176 .. automethod:: cubicweb.server.session.Session.is_hook_activated |
|
1177 |
|
1178 Data manipulation: |
|
1179 |
|
1180 .. automethod:: cubicweb.server.session.Session.add_relation |
|
1181 .. automethod:: cubicweb.server.session.Session.add_relations |
|
1182 .. automethod:: cubicweb.server.session.Session.delete_relation |
|
1183 |
|
1184 Other: |
|
1185 |
|
1186 .. automethod:: cubicweb.server.session.Session.call_service |
|
1187 |
|
1188 |
|
1189 |
|
1190 """ |
|
1191 is_request = False |
|
1192 is_internal_session = False |
|
1193 |
|
1194 def __init__(self, user, repo, cnxprops=None, _id=None): |
|
1195 super(Session, self).__init__(repo.vreg) |
|
1196 self.id = _id or make_uid(unormalize(user.login).encode('UTF8')) |
|
1197 self.user = user |
|
1198 self.repo = repo |
|
1199 self._timestamp = Timestamp() |
|
1200 self.default_mode = 'read' |
|
1201 # short cut to querier .execute method |
|
1202 self._execute = repo.querier.execute |
|
1203 # shared data, used to communicate extra information between the client |
|
1204 # and the rql server |
|
1205 self.data = {} |
|
1206 # i18n initialization |
|
1207 self.set_language(user.prefered_language()) |
|
1208 ### internals |
|
1209 # Connection of this section |
|
1210 self._cnxs = {} |
|
1211 # Data local to the thread |
|
1212 self.__threaddata = threading.local() |
|
1213 self._cnxset_tracker = CnxSetTracker() |
|
1214 self._closed = False |
|
1215 self._lock = threading.RLock() |
|
1216 |
|
1217 def __unicode__(self): |
|
1218 return '<session %s (%s 0x%x)>' % ( |
|
1219 unicode(self.user.login), self.id, id(self)) |
|
1220 @property |
|
1221 def timestamp(self): |
|
1222 return float(self._timestamp) |
|
1223 |
|
1224 @property |
|
1225 def sessionid(self): |
|
1226 return self.id |
|
1227 |
|
1228 @property |
|
1229 def login(self): |
|
1230 # XXX backward compat with dbapi. deprecate me ASAP. |
|
1231 return self.user.login |
|
1232 |
|
1233 def get_cnx(self, cnxid): |
|
1234 """return the <cnxid> connection attached to this session |
|
1235 |
|
1236 Connection is created if necessary""" |
|
1237 with self._lock: # no connection exist with the same id |
|
1238 try: |
|
1239 if self.closed: |
|
1240 raise SessionClosedError('try to access connections set on a closed session %s' % self.id) |
|
1241 cnx = self._cnxs[cnxid] |
|
1242 except KeyError: |
|
1243 cnx = Connection(cnxid, self) |
|
1244 self._cnxs[cnxid] = cnx |
|
1245 return cnx |
|
1246 |
|
1247 def set_cnx(self, cnxid=None): |
|
1248 """set the default connection of the current thread to <cnxid> |
|
1249 |
|
1250 Connection is created if necessary""" |
|
1251 if cnxid is None: |
|
1252 cnxid = threading.currentThread().getName() |
|
1253 self.__threaddata.cnx = self.get_cnx(cnxid) |
|
1254 |
|
1255 @property |
|
1256 def _cnx(self): |
|
1257 """default connection for current session in current thread""" |
|
1258 try: |
|
1259 return self.__threaddata.cnx |
|
1260 except AttributeError: |
|
1261 self.set_cnx() |
|
1262 return self.__threaddata.cnx |
|
1263 |
|
1264 @property |
|
1265 def _current_cnx_id(self): |
|
1266 """TRANSITIONAL PURPOSE""" |
|
1267 try: |
|
1268 return self.__threaddata.cnx.transactionid |
|
1269 except AttributeError: |
|
1270 return None |
|
1271 |
|
1272 def get_option_value(self, option, foreid=None): |
|
1273 return self.repo.get_option_value(option, foreid) |
|
1274 |
|
1275 def transaction(self, free_cnxset=True): |
|
1276 """return context manager to enter a transaction for the session: when |
|
1277 exiting the `with` block on exception, call `session.rollback()`, else |
|
1278 call `session.commit()` on normal exit. |
|
1279 |
|
1280 The `free_cnxset` will be given to rollback/commit methods to indicate |
|
1281 wether the connections set should be freed or not. |
|
1282 """ |
|
1283 return transaction(self, free_cnxset) |
|
1284 |
|
1285 add_relation = cnx_meth('add_relation') |
|
1286 add_relations = cnx_meth('add_relations') |
|
1287 delete_relation = cnx_meth('delete_relation') |
|
1288 |
|
1289 # relations cache handling ################################################# |
|
1290 |
|
1291 update_rel_cache_add = cnx_meth('update_rel_cache_add') |
|
1292 update_rel_cache_del = cnx_meth('update_rel_cache_del') |
|
1293 |
|
1294 # resource accessors ###################################################### |
|
1295 |
|
1296 system_sql = cnx_meth('system_sql') |
|
1297 deleted_in_transaction = cnx_meth('deleted_in_transaction') |
|
1298 added_in_transaction = cnx_meth('added_in_transaction') |
|
1299 rtype_eids_rdef = cnx_meth('rtype_eids_rdef') |
|
1300 |
|
1301 # security control ######################################################### |
|
1302 |
|
1303 |
|
1304 def security_enabled(self, read=None, write=None): |
|
1305 return _session_security_enabled(self, read=read, write=write) |
|
1306 |
|
1307 read_security = cnx_attr('read_security', writable=True) |
|
1308 write_security = cnx_attr('write_security', writable=True) |
|
1309 running_dbapi_query = cnx_attr('running_dbapi_query') |
|
1310 |
|
1311 # hooks activation control ################################################# |
|
1312 # all hooks should be activated during normal execution |
|
1313 |
|
1314 def allow_all_hooks_but(self, *categories): |
|
1315 return _session_hooks_control(self, HOOKS_ALLOW_ALL, *categories) |
|
1316 def deny_all_hooks_but(self, *categories): |
|
1317 return _session_hooks_control(self, HOOKS_DENY_ALL, *categories) |
|
1318 |
|
1319 hooks_mode = cnx_attr('hooks_mode') |
|
1320 |
|
1321 disabled_hook_categories = cnx_attr('disabled_hook_cats') |
|
1322 enabled_hook_categories = cnx_attr('enabled_hook_cats') |
|
1323 disable_hook_categories = cnx_meth('disable_hook_categories') |
|
1324 enable_hook_categories = cnx_meth('enable_hook_categories') |
|
1325 is_hook_category_activated = cnx_meth('is_hook_category_activated') |
|
1326 is_hook_activated = cnx_meth('is_hook_activated') |
|
1327 |
|
1328 # connection management ################################################### |
|
1329 |
|
1330 def keep_cnxset_mode(self, mode): |
|
1331 """set `mode`, e.g. how the session will keep its connections set: |
|
1332 |
|
1333 * if mode == 'write', the connections set is freed after each ready |
|
1334 query, but kept until the transaction's end (eg commit or rollback) |
|
1335 when a write query is detected (eg INSERT/SET/DELETE queries) |
|
1336 |
|
1337 * if mode == 'transaction', the connections set is only freed after the |
|
1338 transaction's end |
|
1339 |
|
1340 notice that a repository has a limited set of connections sets, and a |
|
1341 session has to wait for a free connections set to run any rql query |
|
1342 (unless it already has one set). |
|
1343 """ |
|
1344 assert mode in ('transaction', 'write') |
|
1345 if mode == 'transaction': |
|
1346 self.default_mode = 'transaction' |
|
1347 else: # mode == 'write' |
|
1348 self.default_mode = 'read' |
|
1349 |
|
1350 mode = cnx_attr('mode', writable=True) |
|
1351 commit_state = cnx_attr('commit_state', writable=True) |
|
1352 |
|
1353 @property |
|
1354 def cnxset(self): |
|
1355 """connections set, set according to transaction mode for each query""" |
|
1356 if self._closed: |
|
1357 self.free_cnxset(True) |
|
1358 raise SessionClosedError('try to access connections set on a closed session %s' % self.id) |
|
1359 return self._cnx.cnxset |
|
1360 |
|
1361 def set_cnxset(self): |
|
1362 """the session need a connections set to execute some queries""" |
|
1363 with self._lock: # can probably be removed |
|
1364 if self._closed: |
|
1365 self.free_cnxset(True) |
|
1366 raise SessionClosedError('try to set connections set on a closed session %s' % self.id) |
|
1367 return self._cnx.set_cnxset() |
|
1368 free_cnxset = cnx_meth('free_cnxset') |
|
1369 |
|
1370 def _touch(self): |
|
1371 """update latest session usage timestamp and reset mode to read""" |
|
1372 self._timestamp.touch() |
|
1373 |
|
1374 local_perm_cache = cnx_attr('local_perm_cache') |
|
1375 @local_perm_cache.setter |
|
1376 def local_perm_cache(self, value): |
|
1377 #base class assign an empty dict:-( |
|
1378 assert value == {} |
|
1379 pass |
|
1380 |
|
1381 # shared data handling ################################################### |
|
1382 |
|
1383 def get_shared_data(self, key, default=None, pop=False, txdata=False): |
|
1384 """return value associated to `key` in session data""" |
|
1385 if txdata: |
|
1386 return self._cnx.get_shared_data(key, default, pop, txdata=True) |
|
1387 else: |
|
1388 data = self.data |
|
1389 if pop: |
|
1390 return data.pop(key, default) |
|
1391 else: |
|
1392 return data.get(key, default) |
|
1393 |
|
1394 def set_shared_data(self, key, value, txdata=False): |
|
1395 """set value associated to `key` in session data""" |
|
1396 if txdata: |
|
1397 return self._cnx.set_shared_data(key, value, txdata=True) |
|
1398 else: |
|
1399 self.data[key] = value |
|
1400 |
|
1401 # server-side service call ################################################# |
|
1402 |
|
1403 def call_service(self, regid, **kwargs): |
|
1404 return self.repo._call_service_with_session(self, regid, |
|
1405 **kwargs) |
|
1406 |
|
1407 # request interface ####################################################### |
|
1408 |
|
1409 @property |
|
1410 def cursor(self): |
|
1411 """return a rql cursor""" |
|
1412 return self |
|
1413 |
|
1414 set_entity_cache = cnx_meth('set_entity_cache') |
|
1415 entity_cache = cnx_meth('entity_cache') |
|
1416 cache_entities = cnx_meth('cached_entities') |
|
1417 drop_entity_cache = cnx_meth('drop_entity_cache') |
|
1418 |
|
1419 source_defs = cnx_meth('source_defs') |
|
1420 describe = cnx_meth('describe') |
|
1421 source_from_eid = cnx_meth('source_from_eid') |
|
1422 |
|
1423 |
|
1424 def execute(self, *args, **kwargs): |
|
1425 """db-api like method directly linked to the querier execute method. |
|
1426 |
|
1427 See :meth:`cubicweb.dbapi.Cursor.execute` documentation. |
|
1428 """ |
|
1429 rset = self._cnx.execute(*args, **kwargs) |
|
1430 rset.req = self |
|
1431 return rset |
|
1432 |
|
1433 def close_cnx(self, cnxid): |
|
1434 cnx = self._cnxs.get(cnxid, None) |
|
1435 if cnx is not None: |
|
1436 cnx.free_cnxset(ignoremode=True) |
|
1437 self._clear_thread_storage(cnx) |
|
1438 self._clear_cnx_storage(cnx) |
|
1439 |
|
1440 |
|
1441 def _clear_thread_data(self, free_cnxset=True): |
|
1442 """remove everything from the thread local storage, except connections set |
|
1443 which is explicitly removed by free_cnxset, and mode which is set anyway |
|
1444 by _touch |
|
1445 """ |
|
1446 try: |
|
1447 cnx = self.__threaddata.cnx |
|
1448 except AttributeError: |
|
1449 pass |
|
1450 else: |
|
1451 if free_cnxset: |
|
1452 self.free_cnxset() |
|
1453 if cnx.ctx_count == 0: |
|
1454 self._clear_thread_storage(cnx) |
|
1455 else: |
|
1456 self._clear_cnx_storage(cnx) |
|
1457 else: |
|
1458 self._clear_cnx_storage(cnx) |
|
1459 |
|
1460 def _clear_thread_storage(self, cnx): |
|
1461 self._cnxs.pop(cnx.connectionid, None) |
|
1462 try: |
|
1463 if self.__threaddata.cnx is cnx: |
|
1464 del self.__threaddata.cnx |
|
1465 except AttributeError: |
|
1466 pass |
|
1467 |
|
1468 def _clear_cnx_storage(self, cnx): |
|
1469 cnx.clear() |
|
1470 |
|
1471 def commit(self, free_cnxset=True, reset_pool=None): |
|
1472 """commit the current session's transaction""" |
|
1473 cstate = self._cnx.commit_state |
|
1474 if cstate == 'uncommitable': |
|
1475 raise QueryError('transaction must be rollbacked') |
|
1476 try: |
|
1477 return self._cnx.commit(free_cnxset, reset_pool) |
|
1478 finally: |
1469 self._clear_thread_data(free_cnxset) |
1479 self._clear_thread_data(free_cnxset) |
1470 |
1480 |
1471 def rollback(self, free_cnxset=True, **kwargs): |
1481 def rollback(self, free_cnxset=True, **kwargs): |
1472 """rollback the current session's transaction""" |
1482 """rollback the current session's transaction""" |
1473 try: |
1483 try: |