| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | ## |
|---|
| 3 | ## |
|---|
| 4 | ## This file is part of Indico. |
|---|
| 5 | ## Copyright (C) 2002 - 2013 European Organization for Nuclear Research (CERN). |
|---|
| 6 | ## |
|---|
| 7 | ## Indico is free software; you can redistribute it and/or |
|---|
| 8 | ## modify it under the terms of the GNU General Public License as |
|---|
| 9 | ## published by the Free Software Foundation; either version 3 of the |
|---|
| 10 | ## License, or (at your option) any later version. |
|---|
| 11 | ## |
|---|
| 12 | ## Indico is distributed in the hope that it will be useful, but |
|---|
| 13 | ## WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|---|
| 15 | ## General Public License for more details. |
|---|
| 16 | ## |
|---|
| 17 | ## You should have received a copy of the GNU General Public License |
|---|
| 18 | ## along with Indico;if not, see <http://www.gnu.org/licenses/>. |
|---|
| 19 | |
|---|
| 20 | """This file contains the implementation of some classes dealing with the DB |
|---|
| 21 | so it's usage is easier and more transparent to the rest of the application |
|---|
| 22 | """ |
|---|
| 23 | import os |
|---|
| 24 | import sys |
|---|
| 25 | import threading |
|---|
| 26 | import pkg_resources |
|---|
| 27 | from contextlib import contextmanager |
|---|
| 28 | |
|---|
| 29 | from ZEO.ClientStorage import ClientStorage |
|---|
| 30 | from ZODB.DB import DB |
|---|
| 31 | import transaction |
|---|
| 32 | import ZODB |
|---|
| 33 | |
|---|
| 34 | from MaKaC.consoleScripts.installBase import getIndicoInstallMode |
|---|
| 35 | skip_imports = getIndicoInstallMode() |
|---|
| 36 | |
|---|
| 37 | |
|---|
| 38 | if not skip_imports: |
|---|
| 39 | from MaKaC.common.logger import Logger |
|---|
| 40 | |
|---|
| 41 | |
|---|
| 42 | class MaKaCDB(DB): |
|---|
| 43 | """Subclass of ZODB.DB necessary to remove possible existing dependencies |
|---|
| 44 | from IC""" |
|---|
| 45 | |
|---|
| 46 | def classFactory(self, connection, modulename, globalname): |
|---|
| 47 | if globalname=="PersistentMapping": |
|---|
| 48 | modulename="persistent.mapping" |
|---|
| 49 | elif globalname=="PersistentList": |
|---|
| 50 | modulename="persistent.list" |
|---|
| 51 | elif modulename.startswith("IndexedCatalog.BTrees."): |
|---|
| 52 | modulename="BTrees.%s"%modulename[22:] |
|---|
| 53 | elif modulename.startswith("MaKaC.plugins.EPayment.CERNYellowPay."): |
|---|
| 54 | modulename="indico.ext.epayment.cern.%s"%modulename[len("MaKaC.plugins.EPayment.CERNYellowPay."):] |
|---|
| 55 | return DB.classFactory(self, connection, modulename, globalname) |
|---|
| 56 | |
|---|
| 57 | |
|---|
| 58 | class DBMgr: |
|---|
| 59 | """This class provides the access point to the Shelf (every client will |
|---|
| 60 | use this class in order to obtain a shelf) and some mechanism to |
|---|
| 61 | ensure there is only one connection opened to the DB during a single |
|---|
| 62 | request. |
|---|
| 63 | This class must not be instantiated, an instance can be obtained |
|---|
| 64 | though the "getInstance" method (implements the singleton pattern |
|---|
| 65 | to ensure unicity) |
|---|
| 66 | Needs to be checked if the class (static) attribute _instance is |
|---|
| 67 | thread-safe: as it is shared by all the objects of this class, |
|---|
| 68 | it could provoke concurrency troubles having 2 threads using the db |
|---|
| 69 | connection at the same time. However, the model under which we are |
|---|
| 70 | programming is not multi-threaded (mod_python seems to run different |
|---|
| 71 | interpreters for each apache subprocess, see mod_python doc section |
|---|
| 72 | 4.1) so this thechnique can be used. This has to be taken into account |
|---|
| 73 | when migrating the system to a multi-threading environment. |
|---|
| 74 | """ |
|---|
| 75 | _instance = None |
|---|
| 76 | |
|---|
| 77 | def __init__( self, hostname=None, port=None, max_disconnect_poll=30 ): |
|---|
| 78 | import Configuration # Please leave this import here, db.py is imported during installation process |
|---|
| 79 | cfg = Configuration.Config.getInstance() |
|---|
| 80 | |
|---|
| 81 | if not hostname: |
|---|
| 82 | hostname = cfg.getDBConnectionParams()[0] |
|---|
| 83 | if not port: |
|---|
| 84 | port = cfg.getDBConnectionParams()[1] |
|---|
| 85 | |
|---|
| 86 | self._storage=ClientStorage((hostname, port), username=cfg.getDBUserName(), password=cfg.getDBPassword(), realm=cfg.getDBRealm(), |
|---|
| 87 | max_disconnect_poll=max_disconnect_poll) |
|---|
| 88 | self._db=MaKaCDB(self._storage) |
|---|
| 89 | self._conn = threading.local() |
|---|
| 90 | self._conn.conn = None |
|---|
| 91 | |
|---|
| 92 | @classmethod |
|---|
| 93 | def getInstance( cls, *args, **kwargs ): |
|---|
| 94 | if cls._instance == None: |
|---|
| 95 | Logger.get('dbmgr').debug('cls._instance is None') |
|---|
| 96 | cls._instance=DBMgr(*args, **kwargs) |
|---|
| 97 | return cls._instance |
|---|
| 98 | |
|---|
| 99 | @classmethod |
|---|
| 100 | def setInstance( cls, dbInstance ): |
|---|
| 101 | cls._instance = dbInstance |
|---|
| 102 | |
|---|
| 103 | def _getConnObject(self): |
|---|
| 104 | return self._conn.conn |
|---|
| 105 | |
|---|
| 106 | def _setConnObject(self, obj): |
|---|
| 107 | self._conn.conn = obj |
|---|
| 108 | |
|---|
| 109 | |
|---|
| 110 | def _delConnObject(self): |
|---|
| 111 | self._conn.conn = None |
|---|
| 112 | |
|---|
| 113 | def startRequest( self ): |
|---|
| 114 | """Initialise the DB and starts a new transaction. |
|---|
| 115 | """ |
|---|
| 116 | self._conn.conn = self._db.open() |
|---|
| 117 | |
|---|
| 118 | def endRequest( self, commit=True ): |
|---|
| 119 | """Closes the DB and commits changes. |
|---|
| 120 | """ |
|---|
| 121 | if commit: |
|---|
| 122 | self.commit() |
|---|
| 123 | else: |
|---|
| 124 | self.abort() |
|---|
| 125 | |
|---|
| 126 | self._getConnObject().close() |
|---|
| 127 | self._delConnObject() |
|---|
| 128 | |
|---|
| 129 | def getDBConnection( self ): |
|---|
| 130 | return self._getConnObject() |
|---|
| 131 | |
|---|
| 132 | def isConnected( self ): |
|---|
| 133 | return hasattr(self._conn, 'conn') and self._conn.conn != None |
|---|
| 134 | |
|---|
| 135 | def getDBConnCache(self): |
|---|
| 136 | conn = self._getConnObject() |
|---|
| 137 | return conn._cache |
|---|
| 138 | |
|---|
| 139 | def getDBClassFactory(self): |
|---|
| 140 | return self._db.classFactory |
|---|
| 141 | |
|---|
| 142 | def commit(self, sub=False): |
|---|
| 143 | if (sub): |
|---|
| 144 | transaction.savepoint() |
|---|
| 145 | else: |
|---|
| 146 | transaction.commit() |
|---|
| 147 | |
|---|
| 148 | def commitZODBOld(self, sub=False): |
|---|
| 149 | transaction.commit(sub) |
|---|
| 150 | |
|---|
| 151 | def abort(self): |
|---|
| 152 | transaction.abort() |
|---|
| 153 | |
|---|
| 154 | def sync(self): |
|---|
| 155 | self._getConnObject().sync() |
|---|
| 156 | |
|---|
| 157 | def pack( self, days=1 ): |
|---|
| 158 | self._storage.pack(days=days) |
|---|
| 159 | |
|---|
| 160 | def undoInfo(self, stepNumber=0): |
|---|
| 161 | # One step is made of 1000 transactions. First step is 0 and returns |
|---|
| 162 | # transactions 0 to 999. |
|---|
| 163 | return self._db.undoInfo(stepNumber*1000, (stepNumber+1)*1000) |
|---|
| 164 | |
|---|
| 165 | def undo(self, trans_id): |
|---|
| 166 | self._db.undo(trans_id) |
|---|
| 167 | |
|---|
| 168 | def getDBSize( self ): |
|---|
| 169 | """Return an approximate size of the database, in bytes.""" |
|---|
| 170 | return self._storage.getSize() |
|---|
| 171 | |
|---|
| 172 | def loadObject(self, oid, version): |
|---|
| 173 | return self._storage.load(oid, version) |
|---|
| 174 | |
|---|
| 175 | def storeObject(self, oid, serial, data, version, trans): |
|---|
| 176 | return self._storage.store(oid, serial, data, version, trans) |
|---|
| 177 | |
|---|
| 178 | def tpcBegin(self, trans): |
|---|
| 179 | self._storage.tpc_begin(trans) |
|---|
| 180 | |
|---|
| 181 | def tpcVote(self, trans): |
|---|
| 182 | self._storage.tpc_vote(trans) |
|---|
| 183 | |
|---|
| 184 | def tpcFinish(self, trans): |
|---|
| 185 | self._storage.tpc_finish(trans) |
|---|
| 186 | |
|---|
| 187 | @contextmanager |
|---|
| 188 | def transaction(self, sync=False): |
|---|
| 189 | """ |
|---|
| 190 | context manager (`with`) |
|---|
| 191 | """ |
|---|
| 192 | if sync: |
|---|
| 193 | self.sync() |
|---|
| 194 | yield self.getDBConnection() |
|---|
| 195 | self.commit() |
|---|
| 196 | |
|---|
| 197 | # ZODB version check |
|---|
| 198 | try: |
|---|
| 199 | zodbPkg = pkg_resources.require('ZODB3')[0] |
|---|
| 200 | zodbVersion = zodbPkg.parsed_version |
|---|
| 201 | zodbVersion = (int(zodbVersion[0]), int(zodbVersion[1])) |
|---|
| 202 | except pkg_resources.DistributionNotFound: |
|---|
| 203 | # Very old versions, in which ZODB didn't register |
|---|
| 204 | # with pkg_resources |
|---|
| 205 | import ZODB |
|---|
| 206 | zodbVersion = ZODB.__version__.split('.') |
|---|
| 207 | |
|---|
| 208 | if int(zodbVersion[0]) < 3: |
|---|
| 209 | raise Exception("ZODB 3 required! %s found" % zodbPkg.version) |
|---|
| 210 | elif int(zodbVersion[1]) < 7: |
|---|
| 211 | commit = commitZODBOld |
|---|