Package buildbot :: Package db :: Module dbspec
[frames] | no frames]

Source Code for Module buildbot.db.dbspec

  1  # ***** BEGIN LICENSE BLOCK ***** 
  2  # Version: MPL 1.1/GPL 2.0/LGPL 2.1 
  3  # 
  4  # The contents of this file are subject to the Mozilla Public License Version 
  5  # 1.1 (the "License"); you may not use this file except in compliance with 
  6  # the License. You may obtain a copy of the License at 
  7  # http://www.mozilla.org/MPL/ 
  8  # 
  9  # Software distributed under the License is distributed on an "AS IS" basis, 
 10  # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 
 11  # for the specific language governing rights and limitations under the 
 12  # License. 
 13  # 
 14  # The Original Code is Mozilla-specific Buildbot steps. 
 15  # 
 16  # The Initial Developer of the Original Code is 
 17  # Mozilla Foundation. 
 18  # Portions created by the Initial Developer are Copyright (C) 2009 
 19  # the Initial Developer. All Rights Reserved. 
 20  # 
 21  # Contributor(s): 
 22  #   Brian Warner <warner@lothar.com> 
 23  #   Chris AtLee <catlee@mozilla.com> 
 24  #   Dustin Mitchell <dustin@zmanda.com> 
 25  # 
 26  # Alternatively, the contents of this file may be used under the terms of 
 27  # either the GNU General Public License Version 2 or later (the "GPL"), or 
 28  # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 
 29  # in which case the provisions of the GPL or the LGPL are applicable instead 
 30  # of those above. If you wish to allow use of your version of this file only 
 31  # under the terms of either the GPL or the LGPL, and not to allow others to 
 32  # use your version of this file under the terms of the MPL, indicate your 
 33  # decision by deleting the provisions above and replace them with the notice 
 34  # and other provisions required by the GPL or the LGPL. If you do not delete 
 35  # the provisions above, a recipient may use your version of this file under 
 36  # the terms of any one of the MPL, the GPL or the LGPL. 
 37  # 
 38  # ***** END LICENSE BLOCK ***** 
 39   
 40  import sys, os, cgi, re, time 
 41   
 42  from twisted.python import log, reflect 
 43  from twisted.internet import defer, reactor 
 44  from twisted.enterprise import adbapi 
 45   
 46  from buildbot.db.connector import DBConnector 
 47  from buildbot.db.exceptions import * 
 48  from buildbot import util 
49 50 -class ExpiringConnectionPool(adbapi.ConnectionPool):
51 """ 52 A Connection pool that expires connections after a certain amount of idle 53 time. 54 """
55 - def __init__(self, dbapiName, max_idle=60, *args, **kwargs):
56 """ 57 @param max_idle: reconnect connections that have been idle more than 58 this number of seconds. 59 """ 60 61 log.msg("Using expiring pool with max_idle=%i" % max_idle) 62 63 adbapi.ConnectionPool.__init__(self, dbapiName, *args, **kwargs) 64 self.max_idle = max_idle 65 66 self.connection_lastused = {}
67
68 - def connect(self):
69 tid = self.threadID() 70 now = util.now() 71 lastused = self.connection_lastused.get(tid) 72 if lastused and lastused + self.max_idle < now: 73 conn = self.connections.get(tid) 74 if self.noisy: 75 log.msg("expiring old connection") 76 self.disconnect(conn) 77 78 conn = adbapi.ConnectionPool.connect(self) 79 self.connection_lastused[tid] = now 80 return conn
81
82 - def disconnect(self, conn):
83 adbapi.ConnectionPool.disconnect(self, conn) 84 tid = self.threadID() 85 del self.connection_lastused[tid]
86
87 -class TimeoutError(Exception):
88 - def __init__(self, msg):
89 Exception.__init__(self, msg)
90
91 -class RetryingCursor:
92 max_retry_time = 1800 # Half an hour 93 max_sleep_time = 1 94
95 - def __init__(self, dbapi, cursor):
96 self.dbapi = dbapi 97 self.cursor = cursor
98
99 - def sleep(self, s):
100 time.sleep(s)
101
102 - def execute(self, *args, **kw):
103 start_time = util.now() 104 sleep_time = 0.1 105 while True: 106 try: 107 query_start_time = util.now() 108 result = self.cursor.execute(*args, **kw) 109 end_time = util.now() 110 if end_time - query_start_time > 2: 111 log.msg("Long query (%is): %s" % ((end_time - query_start_time), str((args, kw)))) 112 return result 113 except self.dbapi.OperationalError, e: 114 if e.args[0] == 'database is locked': 115 # Retry 116 log.msg("Retrying query %s" % str((args, kw))) 117 now = util.now() 118 if start_time + self.max_retry_time < now: 119 raise TimeoutError("Exceeded timeout trying to do %s" % str((args, kw))) 120 self.sleep(sleep_time) 121 sleep_time = max(self.max_sleep_time, sleep_time * 2) 122 continue 123 raise
124
125 - def __getattr__(self, name):
126 return getattr(self.cursor, name)
127
128 -class RetryingConnection:
129 - def __init__(self, dbapi, conn):
130 self.dbapi = dbapi 131 self.conn = conn
132
133 - def cursor(self):
134 return RetryingCursor(self.dbapi, self.conn.cursor())
135
136 - def __getattr__(self, name):
137 return getattr(self.conn, name)
138
139 -class RetryingConnectionPool(adbapi.ConnectionPool):
140 - def connect(self):
141 return RetryingConnection(self.dbapi, adbapi.ConnectionPool.connect(self))
142
143 -class DBSpec(object):
144 """ 145 A specification for the database type and other connection parameters. 146 """ 147 148 # List of connkw arguments that are applicable to the connection pool only 149 pool_args = ["max_idle"]
150 - def __init__(self, dbapiName, *connargs, **connkw):
151 # special-case 'sqlite3', replacing it with the available implementation 152 if dbapiName == 'sqlite3': 153 dbapiName = self._get_sqlite_dbapi_name() 154 155 self.dbapiName = dbapiName 156 self.connargs = connargs 157 self.connkw = connkw
158 159 @classmethod
160 - def from_url(cls, url, basedir=None):
161 """ 162 Parses a URL of the format 163 driver://[username:password@]host:port/database[?args] 164 and returns a DB object representing this URL. Percent- 165 substitution will be performed, replacing %(basedir)s with 166 the basedir argument. 167 168 raises ValueError on an invalid URL. 169 """ 170 match = re.match(r""" 171 ^(?P<driver>\w+):// 172 ( 173 ((?P<user>\w+)(:(?P<passwd>\S+))?@)? 174 ((?P<host>[-A-Za-z0-9.]+)(:(?P<port>\d+))?)?/ 175 (?P<database>\S+?)(\?(?P<args>.*))? 176 )?$""", url, re.X) 177 if not match: 178 raise ValueError("Malformed url") 179 180 d = match.groupdict() 181 driver = d['driver'] 182 user = d['user'] 183 passwd = d['passwd'] 184 host = d['host'] 185 port = d['port'] 186 if port is not None: 187 port = int(port) 188 database = d['database'] 189 args = {} 190 if d['args']: 191 for key, value in cgi.parse_qsl(d['args']): 192 args[key] = value 193 194 if driver == "sqlite": 195 # user, passwd, host, and port must all be None 196 if not user == passwd == host == port == None: 197 raise ValueError("user, passwd, host, port must all be None") 198 if not database: 199 database = ":memory:" 200 else: 201 database = database % dict(basedir=basedir) 202 database = os.path.join(basedir, database) 203 return cls("sqlite3", database, **args) 204 elif driver == "mysql": 205 args['host'] = host 206 args['db'] = database 207 if user: 208 args['user'] = user 209 if passwd: 210 args['passwd'] = passwd 211 if port: 212 args['port'] = port 213 if 'max_idle' in args: 214 args['max_idle'] = int(args['max_idle']) 215 216 return cls("MySQLdb", use_unicode=True, charset="utf8", **args) 217 else: 218 raise ValueError("Unsupported dbapi %s" % driver)
219
220 - def _get_sqlite_dbapi_name(self):
221 # see which dbapi we can use and return that name; prefer 222 # pysqlite2.dbapi2 if it is available. 223 sqlite_dbapi_name = None 224 try: 225 from pysqlite2 import dbapi2 as sqlite3 226 sqlite_dbapi_name = "pysqlite2.dbapi2" 227 except ImportError: 228 # don't use built-in sqlite3 on 2.5 -- it has *bad* bugs 229 if sys.version_info >= (2,6): 230 import sqlite3 231 sqlite_dbapi_name = "sqlite3" 232 else: 233 raise 234 return sqlite_dbapi_name
235
236 - def get_dbapi(self):
237 """ 238 Get the dbapi module used for this connection (for things like 239 exceptions and module-global attributes 240 """ 241 return reflect.namedModule(self.dbapiName)
242
243 - def get_sync_connection(self):
244 """ 245 Get a synchronous connection to the specified database. This returns 246 a simple DBAPI connection object. 247 """ 248 dbapi = self.get_dbapi() 249 connkw = self.connkw.copy() 250 for arg in self.pool_args: 251 if arg in connkw: 252 del connkw[arg] 253 conn = dbapi.connect(*self.connargs, **connkw) 254 if 'sqlite' in self.dbapiName: 255 conn = RetryingConnection(dbapi, conn) 256 return conn
257
259 """ 260 Get an asynchronous (adbapi) connection pool for the specified 261 database. 262 """ 263 264 # add some connection keywords 265 connkw = self.connkw.copy() 266 connkw["cp_reconnect"] = True 267 connkw["cp_noisy"] = True 268 269 # This disables sqlite's obsessive checks that a given connection is 270 # only used in one thread; this is justified by the Twisted ticket 271 # regarding the errors you get on connection shutdown if you do *not* 272 # add this parameter: http://twistedmatrix.com/trac/ticket/3629 273 if 'sqlite' in self.dbapiName: 274 connkw['check_same_thread'] = False 275 log.msg("creating adbapi pool: %s %s %s" % \ 276 (self.dbapiName, self.connargs, connkw)) 277 278 # MySQL needs support for expiring idle connections 279 if self.dbapiName == 'MySQLdb': 280 return ExpiringConnectionPool(self.dbapiName, *self.connargs, **connkw) 281 else: 282 return RetryingConnectionPool(self.dbapiName, *self.connargs, **connkw)
283
284 - def get_maxidle(self):
285 default = None 286 if self.dbapiName == "MySQLdb": 287 default = 60 288 return self.connkw.get("max_idle", default)
289