server/session.py
changeset 9092 48d488dd3c51
parent 9091 4a39f3f1f1d9
child 9093 e2f88df79efd
equal deleted inserted replaced
9091:4a39f3f1f1d9 9092:48d488dd3c51
   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:
  1416                         processed.append(operation)
   963                         processed.append(operation)
  1417                         if debug:
   964                         if debug:
  1418                             print operation
   965                             print operation
  1419                         operation.handle_event('precommit_event')
   966                         operation.handle_event('precommit_event')
  1420                     self.pending_operations[:] = processed
   967                     self.pending_operations[:] = processed
  1421                     self.debug('precommit session %s done', self.id)
   968                     self.debug('precommit transaction %s done', self.connectionid)
  1422                 except BaseException:
   969                 except BaseException:
  1423                     # if error on [pre]commit:
   970                     # if error on [pre]commit:
  1424                     #
   971                     #
  1425                     # * set .failed = True on the operation causing the failure
   972                     # * set .failed = True on the operation causing the failure
  1426                     # * call revert<event>_event on processed operations
   973                     # * call revert<event>_event on processed operations
  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: