/apiary/mysql/mysql.py

https://bitbucket.org/lindenlab/apiary/ · Python · 265 lines · 191 code · 32 blank · 42 comment · 35 complexity · 8b84e79b0a09cf3874bc796023ab57a8 MD5 · raw file

  1. #
  2. # $LicenseInfo:firstyear=2010&license=mit$
  3. #
  4. # Copyright (c) 2010, Linden Research, Inc.
  5. #
  6. # Permission is hereby granted, free of charge, to any person obtaining a copy
  7. # of this software and associated documentation files (the "Software"), to deal
  8. # in the Software without restriction, including without limitation the rights
  9. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. # copies of the Software, and to permit persons to whom the Software is
  11. # furnished to do so, subject to the following conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be included in
  14. # all copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. # THE SOFTWARE.
  23. # $/LicenseInfo$
  24. #
  25. from optparse import OptionParser
  26. import MySQLdb
  27. import os
  28. import re
  29. import sys
  30. import time
  31. import apiary
  32. from apiary.tools.debug import *
  33. import sqllog
  34. from apiary.tools import timestamp
  35. def now(future=0.0):
  36. return timestamp.TimeStamp(time.time() + future)
  37. def wait_until(ts):
  38. tn = now()
  39. if ts > tn:
  40. delta = float(ts - tn)
  41. # why was this ever here? --Lex
  42. #if delta > 65.0:
  43. # raise Exception("trying to wait longer than 65s. now = %s, until = %s"
  44. # % (str(tn), str(ts)))
  45. time.sleep(delta)
  46. mysql_host = 'localhost'
  47. mysql_port = 3306
  48. mysql_user = 'guest'
  49. mysql_passwd = ''
  50. mysql_db = 'test'
  51. class MySQLWorkerBee(apiary.WorkerBee):
  52. def __init__(self, options, arguments):
  53. apiary.WorkerBee.__init__(self, options, arguments)
  54. self._connect_options = {}
  55. self._connect_options['host'] = options.mysql_host
  56. self._connect_options['port'] = options.mysql_port
  57. self._connect_options['user'] = options.mysql_user
  58. self._connect_options['passwd'] = options.mysql_passwd
  59. self._connect_options['db'] = options.mysql_db
  60. self._connection = None
  61. self._no_mysql = options.no_mysql
  62. @traced_method
  63. def start(self):
  64. self._error = False
  65. self._errormsg = ''
  66. if self._no_mysql:
  67. return
  68. try:
  69. if self._connection is None and not self._no_mysql:
  70. connection = MySQLdb.connect(**self._connect_options)
  71. self._connection = connection
  72. pass
  73. except Exception, e: # more restrictive error catching?
  74. self._error = True
  75. self._errormsg = "500 " + str(e)
  76. @traced_method
  77. def event(self, data):
  78. if self._error:
  79. return
  80. try:
  81. (tstr, sql) = data.split("\t", 1)
  82. if not self._asap:
  83. self.log("waiting")
  84. wait_until(timestamp.TimeStamp(tstr))
  85. sql = sql.strip()
  86. if sql:
  87. self.log("executing SQL: " + sql)
  88. self.execute_sql(sql)
  89. except Exception, e: # more restrictive error catching?
  90. self._error = True
  91. self._errormsg = "501 " + str(e)
  92. @traced_method
  93. def end(self):
  94. if self._no_mysql:
  95. return "200 OK"
  96. try:
  97. if True:
  98. self._connection.close()
  99. self._connection = None
  100. except Exception, e:
  101. if not self._error:
  102. self._error = True
  103. self._errormsg = "502 " + str(e)
  104. if self._error:
  105. return self._errormsg
  106. return '200 OK'
  107. @traced_method
  108. def execute_sql(self, sql):
  109. """Execute an SQL statement.
  110. Subclasses may override this to alter the SQL before being executed.
  111. If so, they should call this inherited version to actually execute
  112. the statement. It is acceptable for a sub-class to call this version
  113. multiple times.
  114. Exceptions are caught in the outer calling function (event())
  115. """
  116. if self._no_mysql:
  117. return
  118. cursor = self._connection.cursor()
  119. cursor.execute(sql)
  120. try:
  121. cursor.fetchall()
  122. except:
  123. pass # not all SQL has data to fetch
  124. cursor.close()
  125. class MySQLQueenBee(apiary.QueenBee):
  126. def __init__(self, options, arguments):
  127. apiary.QueenBee.__init__(self, options, arguments)
  128. if options.pickled:
  129. self._events = sqllog.input_pickled_events(arguments)
  130. else:
  131. self._events = sqllog.input_events(arguments)
  132. # this builds a sequence of events from the log streams in the
  133. # arguments, which come here from the command line
  134. self._connections = {}
  135. self._tally = {}
  136. self._time_scale = 1.0 / options.speedup
  137. self._event_start = None
  138. self._replay_start = None
  139. self._tally_time = time.time() + 15.0
  140. self._delay_start = timestamp.TimeStamp(options.prefill)
  141. def tally(self, msg):
  142. # aggregate these error codes since we see a lot of them (1062/1064)
  143. if "Duplicate entry" in msg:
  144. msg = '501 (1062, "Duplicate entry for key")'
  145. if "You have an error in your SQL syntax" in msg:
  146. msg = '501 (1064, "You have an error in your SQL syntax")'
  147. self._tally[msg] = self._tally.get(msg, 0) + 1
  148. if time.time() > self._tally_time:
  149. self.print_tally()
  150. def print_tally(self):
  151. keys = self._tally.keys()
  152. keys.sort()
  153. print
  154. print " count - message"
  155. print "------------ -------------------------------------------"
  156. for k in keys:
  157. print ("%12d - %s" % (self._tally[k], k))
  158. self._tally_time = time.time() + 15.0
  159. @traced_method
  160. def next(self):
  161. try:
  162. while True:
  163. e = self._events.next() # e is a sqllog.Event object
  164. if e.state != sqllog.Event.Response:
  165. break
  166. except StopIteration:
  167. return False
  168. if self._event_start is None:
  169. self._event_start = e.time
  170. t = (e.time - self._event_start) * self._time_scale + self._replay_start + self._delay_start
  171. id = e.id
  172. if e.state == sqllog.Event.End:
  173. if id in self._connections:
  174. self.tally("102 End connection")
  175. del self._connections[id]
  176. self.end(id)
  177. else:
  178. self.tally("103 Duplicate end")
  179. else:
  180. if id not in self._connections:
  181. self.tally("100 Start connection")
  182. s = self._connections[id] = True
  183. self.start(id)
  184. self.tally("101 Event")
  185. self.event(id, str(t) + "\t" + e.body)
  186. return True
  187. def result(self, seq, d):
  188. self.tally(d)
  189. @traced_method
  190. def main(self):
  191. t = - time.time()
  192. c = - time.clock()
  193. self._replay_start = now(1.0)
  194. apiary.QueenBee.main(self)
  195. c += time.clock()
  196. t += time.time()
  197. print ("Timing: %f process clock, %f wall clock" % (c, t))
  198. self.print_tally()
  199. # Plugin interface:
  200. queenbee_cls = MySQLQueenBee
  201. workerbee_cls = MySQLWorkerBee
  202. def add_options(parser):
  203. parser.add_option('--no-mysql', default=False, dest='no_mysql', action='store_true',
  204. help="Don't make mysql connections. Return '200 OK' instead.")
  205. parser.add_option('--speedup', default=1.0, dest='speedup', type='float',
  206. help="Time multiple used when replaying query logs. 2.0 means "
  207. "that queries run twice as fast (and the entire run takes "
  208. "half the time the capture ran for).")
  209. parser.add_option('--pickled',
  210. default=False, action='store_true',
  211. help='sequence file contains pickled events')
  212. parser.add_option('--prefill',
  213. default=0, type="int", metavar="SECONDS",
  214. help="""Prefill the queue with jobs for SECONDS. Use
  215. this if the queenbee can't generate jobs fast enough.""")
  216. parser.add_option('--mysql-host',
  217. default=mysql_host, metavar='HOST',
  218. help='MySQL server to connect to (default: %default)')
  219. parser.add_option('--mysql-port',
  220. default=mysql_port, metavar='PORT',
  221. help='MySQL port to connect on (default: %default)')
  222. parser.add_option('--mysql-user',
  223. default=mysql_user, metavar='USER',
  224. help='MySQL user to connect as (default: %default)')
  225. parser.add_option('--mysql-passwd',
  226. default=mysql_passwd, metavar='PW',
  227. help='MySQL password to connect with (default: %default)')
  228. parser.add_option('--mysql-db',
  229. default=mysql_db, metavar='DB',
  230. help='MySQL database to connect to (default: %default)')