1224 |
1223 |
1225 def __float__(self): |
1224 def __float__(self): |
1226 return float(self.value) |
1225 return float(self.value) |
1227 |
1226 |
1228 |
1227 |
1229 class Session(RequestSessionBase): # XXX repoapi: stop being a |
1228 class Session(object): |
1230 # RequestSessionBase at some point |
|
1231 """Repository user session |
1229 """Repository user session |
1232 |
1230 |
1233 This ties all together: |
1231 This ties all together: |
1234 * session id, |
1232 * session id, |
1235 * user, |
1233 * user, |
1236 * connections set, |
|
1237 * other session data. |
1234 * other session data. |
1238 |
|
1239 **About session storage / transactions** |
|
1240 |
|
1241 Here is a description of internal session attributes. Besides :attr:`data` |
|
1242 and :attr:`transaction_data`, you should not have to use attributes |
|
1243 described here but higher level APIs. |
|
1244 |
|
1245 :attr:`data` is a dictionary containing shared data, used to communicate |
|
1246 extra information between the client and the repository |
|
1247 |
|
1248 :attr:`_cnxs` is a dictionary of :class:`Connection` instance, one |
|
1249 for each running connection. The key is the connection id. By default |
|
1250 the connection id is the thread name but it can be otherwise (per dbapi |
|
1251 cursor for instance, or per thread name *from another process*). |
|
1252 |
|
1253 :attr:`__threaddata` is a thread local storage whose `cnx` attribute |
|
1254 refers to the proper instance of :class:`Connection` according to the |
|
1255 connection. |
|
1256 |
|
1257 You should not have to use neither :attr:`_cnx` nor :attr:`__threaddata`, |
|
1258 simply access connection data transparently through the :attr:`_cnx` |
|
1259 property. Also, you usually don't have to access it directly since current |
|
1260 connection's data may be accessed/modified through properties / methods: |
|
1261 |
|
1262 :attr:`connection_data`, similarly to :attr:`data`, is a dictionary |
|
1263 containing some shared data that should be cleared at the end of the |
|
1264 connection. Hooks and operations may put arbitrary data in there, and |
|
1265 this may also be used as a communication channel between the client and |
|
1266 the repository. |
|
1267 |
|
1268 .. automethod:: cubicweb.server.session.Session.get_shared_data |
|
1269 .. automethod:: cubicweb.server.session.Session.set_shared_data |
|
1270 .. automethod:: cubicweb.server.session.Session.added_in_transaction |
|
1271 .. automethod:: cubicweb.server.session.Session.deleted_in_transaction |
|
1272 |
|
1273 Connection state information: |
|
1274 |
|
1275 :attr:`running_dbapi_query`, boolean flag telling if the executing query |
|
1276 is coming from a dbapi connection or is a query from within the repository |
|
1277 |
|
1278 :attr:`cnxset`, the connections set to use to execute queries on sources. |
|
1279 During a transaction, the connection set may be freed so that is may be |
|
1280 used by another session as long as no writing is done. This means we can |
|
1281 have multiple sessions with a reasonably low connections set pool size. |
|
1282 |
|
1283 .. automethod:: cubicweb.server.session.Session.set_cnxset |
|
1284 .. automethod:: cubicweb.server.session.Session.free_cnxset |
|
1285 |
|
1286 :attr:`mode`, string telling the connections set handling mode, may be one |
|
1287 of 'read' (connections set may be freed), 'write' (some write was done in |
|
1288 the connections set, it can't be freed before end of the transaction), |
|
1289 'transaction' (we want to keep the connections set during all the |
|
1290 transaction, with or without writing) |
|
1291 |
|
1292 :attr:`pending_operations`, ordered list of operations to be processed on |
|
1293 commit/rollback |
|
1294 |
|
1295 :attr:`commit_state`, describing the transaction commit state, may be one |
|
1296 of None (not yet committing), 'precommit' (calling precommit event on |
|
1297 operations), 'postcommit' (calling postcommit event on operations), |
|
1298 'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error |
|
1299 has been raised during the transaction and so it must be rolled back). |
|
1300 |
|
1301 .. automethod:: cubicweb.server.session.Session.commit |
|
1302 .. automethod:: cubicweb.server.session.Session.rollback |
|
1303 .. automethod:: cubicweb.server.session.Session.close |
|
1304 .. automethod:: cubicweb.server.session.Session.closed |
|
1305 |
|
1306 Security level Management: |
|
1307 |
|
1308 :attr:`read_security` and :attr:`write_security`, boolean flags telling if |
|
1309 read/write security is currently activated. |
|
1310 |
|
1311 .. automethod:: cubicweb.server.session.Session.security_enabled |
|
1312 |
|
1313 Hooks Management: |
|
1314 |
|
1315 :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`. |
|
1316 |
|
1317 :attr:`enabled_hook_categories`, when :attr:`hooks_mode` is |
|
1318 `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled. |
|
1319 |
|
1320 :attr:`disabled_hook_categories`, when :attr:`hooks_mode` is |
|
1321 `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled. |
|
1322 |
|
1323 .. automethod:: cubicweb.server.session.Session.deny_all_hooks_but |
|
1324 .. automethod:: cubicweb.server.session.Session.allow_all_hooks_but |
|
1325 .. automethod:: cubicweb.server.session.Session.is_hook_category_activated |
|
1326 .. automethod:: cubicweb.server.session.Session.is_hook_activated |
|
1327 |
|
1328 Data manipulation: |
|
1329 |
|
1330 .. automethod:: cubicweb.server.session.Session.add_relation |
|
1331 .. automethod:: cubicweb.server.session.Session.add_relations |
|
1332 .. automethod:: cubicweb.server.session.Session.delete_relation |
|
1333 |
|
1334 Other: |
|
1335 |
|
1336 .. automethod:: cubicweb.server.session.Session.call_service |
|
1337 |
|
1338 |
|
1339 |
|
1340 """ |
1235 """ |
1341 is_request = False |
|
1342 |
1236 |
1343 def __init__(self, user, repo, cnxprops=None, _id=None): |
1237 def __init__(self, user, repo, cnxprops=None, _id=None): |
1344 super(Session, self).__init__(repo.vreg) |
|
1345 self.sessionid = _id or make_uid(unormalize(user.login).encode('UTF8')) |
1238 self.sessionid = _id or make_uid(unormalize(user.login).encode('UTF8')) |
1346 self.user = user # XXX repoapi: deprecated and store only a login. |
1239 self.user = user # XXX repoapi: deprecated and store only a login. |
1347 self.repo = repo |
1240 self.repo = repo |
|
1241 self.vreg = repo.vreg |
1348 self._timestamp = Timestamp() |
1242 self._timestamp = Timestamp() |
1349 self.default_mode = 'read' |
|
1350 # short cut to querier .execute method |
|
1351 self._execute = repo.querier.execute |
|
1352 # shared data, used to communicate extra information between the client |
|
1353 # and the rql server |
|
1354 self.data = {} |
1243 self.data = {} |
1355 # i18n initialization |
1244 self.closed = False |
1356 self.set_language(user.prefered_language()) |
1245 |
1357 ### internals |
1246 def close(self): |
1358 # Connection of this section |
1247 self.closed = True |
1359 self._cnxs = {} # XXX repoapi: remove this when nobody use the session |
1248 |
1360 # as a Connection |
1249 def __enter__(self): |
1361 # Data local to the thread |
1250 return self |
1362 self.__threaddata = threading.local() # XXX repoapi: remove this when |
1251 |
1363 # nobody use the session as a Connection |
1252 def __exit__(self, *args): |
1364 self._cnxset_tracker = CnxSetTracker() |
1253 pass |
1365 self._closed = False |
|
1366 self._lock = threading.RLock() |
|
1367 |
1254 |
1368 def __unicode__(self): |
1255 def __unicode__(self): |
1369 return '<session %s (%s 0x%x)>' % ( |
1256 return '<session %s (%s 0x%x)>' % ( |
1370 unicode(self.user.login), self.sessionid, id(self)) |
1257 unicode(self.user.login), self.sessionid, id(self)) |
|
1258 |
1371 @property |
1259 @property |
1372 def timestamp(self): |
1260 def timestamp(self): |
1373 return float(self._timestamp) |
1261 return float(self._timestamp) |
1374 |
1262 |
1375 @property |
1263 @property |
1385 """Return a new Connection object linked to the session |
1273 """Return a new Connection object linked to the session |
1386 |
1274 |
1387 The returned Connection will *not* be managed by the Session. |
1275 The returned Connection will *not* be managed by the Session. |
1388 """ |
1276 """ |
1389 return Connection(self) |
1277 return Connection(self) |
1390 |
|
1391 def _get_cnx(self, cnxid): |
|
1392 """return the <cnxid> connection attached to this session |
|
1393 |
|
1394 Connection is created if necessary""" |
|
1395 with self._lock: # no connection exist with the same id |
|
1396 try: |
|
1397 if self.closed: |
|
1398 raise SessionClosedError('try to access connections set on' |
|
1399 ' a closed session %s' % self.id) |
|
1400 cnx = self._cnxs[cnxid] |
|
1401 assert cnx._session_handled |
|
1402 except KeyError: |
|
1403 cnx = Connection(self, cnxid=cnxid, session_handled=True) |
|
1404 self._cnxs[cnxid] = cnx |
|
1405 cnx.__enter__() |
|
1406 return cnx |
|
1407 |
|
1408 def _close_cnx(self, cnx): |
|
1409 """Close a Connection related to a session""" |
|
1410 assert cnx._session_handled |
|
1411 cnx.__exit__() |
|
1412 self._cnxs.pop(cnx.connectionid, None) |
|
1413 try: |
|
1414 if self.__threaddata.cnx is cnx: |
|
1415 del self.__threaddata.cnx |
|
1416 except AttributeError: |
|
1417 pass |
|
1418 |
|
1419 def set_cnx(self, cnxid=None): |
|
1420 # XXX repoapi: remove this when nobody use the session as a Connection |
|
1421 """set the default connection of the current thread to <cnxid> |
|
1422 |
|
1423 Connection is created if necessary""" |
|
1424 if cnxid is None: |
|
1425 cnxid = threading.currentThread().getName() |
|
1426 cnx = self._get_cnx(cnxid) |
|
1427 # New style session should not be accesed through the session. |
|
1428 assert cnx._session_handled |
|
1429 self.__threaddata.cnx = cnx |
|
1430 |
|
1431 @property |
|
1432 def _cnx(self): |
|
1433 """default connection for current session in current thread""" |
|
1434 try: |
|
1435 return self.__threaddata.cnx |
|
1436 except AttributeError: |
|
1437 self.set_cnx() |
|
1438 return self.__threaddata.cnx |
|
1439 |
1278 |
1440 @deprecated('[3.19] use a Connection object instead') |
1279 @deprecated('[3.19] use a Connection object instead') |
1441 def get_option_value(self, option, foreid=None): |
1280 def get_option_value(self, option, foreid=None): |
1442 if foreid is not None: |
1281 if foreid is not None: |
1443 warn('[3.19] foreid argument is deprecated', DeprecationWarning, |
1282 warn('[3.19] foreid argument is deprecated', DeprecationWarning, |
1444 stacklevel=2) |
1283 stacklevel=2) |
1445 return self.repo.get_option_value(option) |
1284 return self.repo.get_option_value(option) |
1446 |
1285 |
1447 @deprecated('[3.19] use a Connection object instead') |
|
1448 def transaction(self, free_cnxset=True): |
|
1449 """return context manager to enter a transaction for the session: when |
|
1450 exiting the `with` block on exception, call `session.rollback()`, else |
|
1451 call `session.commit()` on normal exit. |
|
1452 |
|
1453 The `free_cnxset` will be given to rollback/commit methods to indicate |
|
1454 whether the connections set should be freed or not. |
|
1455 """ |
|
1456 return transaction(self, free_cnxset) |
|
1457 |
|
1458 add_relation = cnx_meth('add_relation') |
|
1459 add_relations = cnx_meth('add_relations') |
|
1460 delete_relation = cnx_meth('delete_relation') |
|
1461 |
|
1462 # relations cache handling ################################################# |
|
1463 |
|
1464 update_rel_cache_add = cnx_meth('update_rel_cache_add') |
|
1465 update_rel_cache_del = cnx_meth('update_rel_cache_del') |
|
1466 |
|
1467 # resource accessors ###################################################### |
|
1468 |
|
1469 system_sql = cnx_meth('system_sql') |
|
1470 deleted_in_transaction = cnx_meth('deleted_in_transaction') |
|
1471 added_in_transaction = cnx_meth('added_in_transaction') |
|
1472 rtype_eids_rdef = cnx_meth('rtype_eids_rdef') |
|
1473 |
|
1474 # security control ######################################################### |
|
1475 |
|
1476 @deprecated('[3.19] use a Connection object instead') |
|
1477 def security_enabled(self, read=None, write=None): |
|
1478 return _session_security_enabled(self, read=read, write=write) |
|
1479 |
|
1480 read_security = cnx_attr('read_security', writable=True) |
|
1481 write_security = cnx_attr('write_security', writable=True) |
|
1482 running_dbapi_query = cnx_attr('running_dbapi_query') |
|
1483 |
|
1484 # hooks activation control ################################################# |
|
1485 # all hooks should be activated during normal execution |
|
1486 |
|
1487 |
|
1488 @deprecated('[3.19] use a Connection object instead') |
|
1489 def allow_all_hooks_but(self, *categories): |
|
1490 return _session_hooks_control(self, HOOKS_ALLOW_ALL, *categories) |
|
1491 @deprecated('[3.19] use a Connection object instead') |
|
1492 def deny_all_hooks_but(self, *categories): |
|
1493 return _session_hooks_control(self, HOOKS_DENY_ALL, *categories) |
|
1494 |
|
1495 hooks_mode = cnx_attr('hooks_mode') |
|
1496 |
|
1497 disabled_hook_categories = cnx_attr('disabled_hook_cats') |
|
1498 enabled_hook_categories = cnx_attr('enabled_hook_cats') |
|
1499 disable_hook_categories = cnx_meth('disable_hook_categories') |
|
1500 enable_hook_categories = cnx_meth('enable_hook_categories') |
|
1501 is_hook_category_activated = cnx_meth('is_hook_category_activated') |
|
1502 is_hook_activated = cnx_meth('is_hook_activated') |
|
1503 |
|
1504 # connection management ################################################### |
|
1505 |
|
1506 @deprecated('[3.19] use a Connection object instead') |
|
1507 def keep_cnxset_mode(self, mode): |
|
1508 """set `mode`, e.g. how the session will keep its connections set: |
|
1509 |
|
1510 * if mode == 'write', the connections set is freed after each read |
|
1511 query, but kept until the transaction's end (eg commit or rollback) |
|
1512 when a write query is detected (eg INSERT/SET/DELETE queries) |
|
1513 |
|
1514 * if mode == 'transaction', the connections set is only freed after the |
|
1515 transaction's end |
|
1516 |
|
1517 notice that a repository has a limited set of connections sets, and a |
|
1518 session has to wait for a free connections set to run any rql query |
|
1519 (unless it already has one set). |
|
1520 """ |
|
1521 assert mode in ('transaction', 'write') |
|
1522 if mode == 'transaction': |
|
1523 self.default_mode = 'transaction' |
|
1524 else: # mode == 'write' |
|
1525 self.default_mode = 'read' |
|
1526 |
|
1527 mode = cnx_attr('mode', writable=True) |
|
1528 commit_state = cnx_attr('commit_state', writable=True) |
|
1529 |
|
1530 @property |
|
1531 @deprecated('[3.19] use a Connection object instead') |
|
1532 def cnxset(self): |
|
1533 """connections set, set according to transaction mode for each query""" |
|
1534 if self._closed: |
|
1535 self.free_cnxset(True) |
|
1536 raise SessionClosedError('try to access connections set on a closed session %s' % self.id) |
|
1537 return self._cnx.cnxset |
|
1538 |
|
1539 def set_cnxset(self): |
|
1540 """the session need a connections set to execute some queries""" |
|
1541 with self._lock: # can probably be removed |
|
1542 if self._closed: |
|
1543 self.free_cnxset(True) |
|
1544 raise SessionClosedError('try to set connections set on a closed session %s' % self.id) |
|
1545 return self._cnx.set_cnxset() |
|
1546 free_cnxset = cnx_meth('free_cnxset') |
|
1547 ensure_cnx_set = cnx_attr('ensure_cnx_set') |
|
1548 |
|
1549 def _touch(self): |
1286 def _touch(self): |
1550 """update latest session usage timestamp and reset mode to read""" |
1287 """update latest session usage timestamp and reset mode to read""" |
1551 self._timestamp.touch() |
1288 self._timestamp.touch() |
1552 |
1289 |
1553 local_perm_cache = cnx_attr('local_perm_cache') |
1290 local_perm_cache = cnx_attr('local_perm_cache') |
1554 @local_perm_cache.setter |
1291 @local_perm_cache.setter |
1555 def local_perm_cache(self, value): |
1292 def local_perm_cache(self, value): |
1556 #base class assign an empty dict:-( |
1293 #base class assign an empty dict:-( |
1557 assert value == {} |
1294 assert value == {} |
1558 pass |
1295 pass |
1559 |
|
1560 # shared data handling ################################################### |
|
1561 |
|
1562 @deprecated('[3.19] use session or transaction data') |
|
1563 def get_shared_data(self, key, default=None, pop=False, txdata=False): |
|
1564 """return value associated to `key` in session data""" |
|
1565 if txdata: |
|
1566 return self._cnx.get_shared_data(key, default, pop, txdata=True) |
|
1567 else: |
|
1568 data = self.data |
|
1569 if pop: |
|
1570 return data.pop(key, default) |
|
1571 else: |
|
1572 return data.get(key, default) |
|
1573 |
|
1574 @deprecated('[3.19] use session or transaction data') |
|
1575 def set_shared_data(self, key, value, txdata=False): |
|
1576 """set value associated to `key` in session data""" |
|
1577 if txdata: |
|
1578 return self._cnx.set_shared_data(key, value, txdata=True) |
|
1579 else: |
|
1580 self.data[key] = value |
|
1581 |
|
1582 # server-side service call ################################################# |
|
1583 |
|
1584 call_service = cnx_meth('call_service') |
|
1585 |
|
1586 # request interface ####################################################### |
|
1587 |
|
1588 @property |
|
1589 @deprecated('[3.19] use a Connection object instead') |
|
1590 def cursor(self): |
|
1591 """return a rql cursor""" |
|
1592 return self |
|
1593 |
|
1594 set_entity_cache = cnx_meth('set_entity_cache') |
|
1595 entity_cache = cnx_meth('entity_cache') |
|
1596 cache_entities = cnx_meth('cached_entities') |
|
1597 drop_entity_cache = cnx_meth('drop_entity_cache') |
|
1598 |
|
1599 source_defs = cnx_meth('source_defs') |
|
1600 entity_metas = cnx_meth('entity_metas') |
|
1601 describe = cnx_meth('describe') # XXX deprecated in 3.19 |
|
1602 |
|
1603 |
|
1604 @deprecated('[3.19] use a Connection object instead') |
|
1605 def execute(self, *args, **kwargs): |
|
1606 """db-api like method directly linked to the querier execute method. |
|
1607 |
|
1608 See :meth:`cubicweb.dbapi.Cursor.execute` documentation. |
|
1609 """ |
|
1610 rset = self._cnx.execute(*args, **kwargs) |
|
1611 rset.req = self |
|
1612 return rset |
|
1613 |
|
1614 def _clear_thread_data(self, free_cnxset=True): |
|
1615 """remove everything from the thread local storage, except connections set |
|
1616 which is explicitly removed by free_cnxset, and mode which is set anyway |
|
1617 by _touch |
|
1618 """ |
|
1619 try: |
|
1620 cnx = self.__threaddata.cnx |
|
1621 except AttributeError: |
|
1622 pass |
|
1623 else: |
|
1624 if free_cnxset: |
|
1625 cnx._free_cnxset() |
|
1626 if cnx.ctx_count == 0: |
|
1627 self._close_cnx(cnx) |
|
1628 else: |
|
1629 cnx.clear() |
|
1630 else: |
|
1631 cnx.clear() |
|
1632 |
|
1633 @deprecated('[3.19] use a Connection object instead') |
|
1634 def commit(self, free_cnxset=True, reset_pool=None): |
|
1635 """commit the current session's transaction""" |
|
1636 cstate = self._cnx.commit_state |
|
1637 if cstate == 'uncommitable': |
|
1638 raise QueryError('transaction must be rolled back') |
|
1639 try: |
|
1640 return self._cnx.commit(free_cnxset, reset_pool) |
|
1641 finally: |
|
1642 self._clear_thread_data(free_cnxset) |
|
1643 |
|
1644 @deprecated('[3.19] use a Connection object instead') |
|
1645 def rollback(self, *args, **kwargs): |
|
1646 """rollback the current session's transaction""" |
|
1647 return self._rollback(*args, **kwargs) |
|
1648 |
|
1649 def _rollback(self, free_cnxset=True, **kwargs): |
|
1650 try: |
|
1651 return self._cnx.rollback(free_cnxset, **kwargs) |
|
1652 finally: |
|
1653 self._clear_thread_data(free_cnxset) |
|
1654 |
|
1655 def close(self): |
|
1656 # do not close connections set on session close, since they are shared now |
|
1657 tracker = self._cnxset_tracker |
|
1658 with self._lock: |
|
1659 self._closed = True |
|
1660 tracker.close() |
|
1661 if self._cnx._session_handled: |
|
1662 self._rollback() |
|
1663 self.debug('waiting for open connection of session: %s', self) |
|
1664 timeout = 10 |
|
1665 pendings = tracker.wait(timeout) |
|
1666 if pendings: |
|
1667 self.error('%i connection still alive after 10 seconds, will close ' |
|
1668 'session anyway', len(pendings)) |
|
1669 for cnxid in pendings: |
|
1670 cnx = self._cnxs.get(cnxid) |
|
1671 if cnx is not None: |
|
1672 # drop cnx.cnxset |
|
1673 with tracker: |
|
1674 try: |
|
1675 cnxset = cnx.cnxset |
|
1676 if cnxset is None: |
|
1677 continue |
|
1678 cnx.cnxset = None |
|
1679 except RuntimeError: |
|
1680 msg = 'issue while force free of cnxset in %s' |
|
1681 self.error(msg, cnx) |
|
1682 # cnxset.reconnect() do an hard reset of the cnxset |
|
1683 # it force it to be freed |
|
1684 cnxset.reconnect() |
|
1685 self.repo._free_cnxset(cnxset) |
|
1686 del self.__threaddata |
|
1687 del self._cnxs |
|
1688 |
|
1689 @property |
|
1690 def closed(self): |
|
1691 return not hasattr(self, '_cnxs') |
|
1692 |
|
1693 # transaction data/operations management ################################## |
|
1694 |
|
1695 transaction_data = cnx_attr('transaction_data') |
|
1696 pending_operations = cnx_attr('pending_operations') |
|
1697 pruned_hooks_cache = cnx_attr('pruned_hooks_cache') |
|
1698 add_operation = cnx_meth('add_operation') |
|
1699 |
|
1700 # undo support ############################################################ |
|
1701 |
|
1702 ertype_supports_undo = cnx_meth('ertype_supports_undo') |
|
1703 transaction_inc_action_counter = cnx_meth('transaction_inc_action_counter') |
|
1704 transaction_uuid = cnx_meth('transaction_uuid') |
|
1705 |
|
1706 # querier helpers ######################################################### |
|
1707 |
|
1708 rql_rewriter = cnx_attr('_rewriter') |
|
1709 |
1296 |
1710 # deprecated ############################################################### |
1297 # deprecated ############################################################### |
1711 |
1298 |
1712 @property |
1299 @property |
1713 def anonymous_session(self): |
1300 def anonymous_session(self): |