From dd54f9ff13ee0d377480f033a49cc4b5aa8abe5a Mon Sep 17 00:00:00 2001 From: Alan Hamlett Date: Tue, 12 May 2015 14:58:20 -0700 Subject: [PATCH] reuse SSL handshake across multiple processes for improved performance --- wakatime/base.py | 15 ++++-- wakatime/offlinequeue.py | 7 ++- wakatime/session_cache.py | 109 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 wakatime/session_cache.py diff --git a/wakatime/base.py b/wakatime/base.py index 9ac3d98..30186c8 100644 --- a/wakatime/base.py +++ b/wakatime/base.py @@ -29,14 +29,14 @@ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pac from .__about__ import __version__ from .compat import u, open, is_py3 -from .offlinequeue import Queue from .logger import setup_logging -from .project import find_project -from .stats import get_file_stats +from .offlinequeue import Queue from .packages import argparse from .packages import simplejson as json -from .packages import requests from .packages.requests.exceptions import RequestException +from .project import find_project +from .session_cache import SessionCache +from .stats import get_file_stats try: from .packages import tzlocal except: @@ -360,10 +360,13 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non if tz: headers['TimeZone'] = u(tz.zone) + session_cache = SessionCache() + session = session_cache.get() + # log time to api response = None try: - response = requests.post(api_url, data=request_body, headers=headers, + response = session.post(api_url, data=request_body, headers=headers, proxies=proxies) except RequestException: exception_data = { @@ -385,6 +388,7 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non log.debug({ 'response_code': response_code, }) + session_cache.save(session) return True if offline: if response_code != 400: @@ -410,6 +414,7 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non 'response_code': response_code, 'response_content': response_content, }) + session_cache.delete() return False diff --git a/wakatime/offlinequeue.py b/wakatime/offlinequeue.py index 2f57ab3..ebbc2d3 100644 --- a/wakatime/offlinequeue.py +++ b/wakatime/offlinequeue.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- """ - wakatime.queue - ~~~~~~~~~~~~~~ + wakatime.offlinequeue + ~~~~~~~~~~~~~~~~~~~~~ - Queue for offline time logging. - http://wakatime.com + Queue for saving heartbeats while offline. :copyright: (c) 2014 Alan Hamlett. :license: BSD, see LICENSE for more details. diff --git a/wakatime/session_cache.py b/wakatime/session_cache.py new file mode 100644 index 0000000..7986fa0 --- /dev/null +++ b/wakatime/session_cache.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +""" + wakatime.session_cache + ~~~~~~~~~~~~~~~~~~~~~~ + + Persist requests.Session for multiprocess SSL handshake pooling. + + :copyright: (c) 2015 Alan Hamlett. + :license: BSD, see LICENSE for more details. +""" + + +import logging +import os +import pickle +import sys +import traceback + +try: + import sqlite3 + HAS_SQL = True +except ImportError: + HAS_SQL = False + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages')) + +from .packages import requests + + +log = logging.getLogger('WakaTime') + + +class SessionCache(object): + DB_FILE = os.path.join(os.path.expanduser('~'), '.wakatime.db') + + def connect(self): + conn = sqlite3.connect(self.DB_FILE) + c = conn.cursor() + c.execute('''CREATE TABLE IF NOT EXISTS session ( + value BLOB) + ''') + return (conn, c) + + + def save(self, session): + """Saves a requests.Session object for the next heartbeat process. + """ + + if not HAS_SQL: + return + try: + conn, c = self.connect() + c.execute('DELETE FROM session') + values = { + 'value': pickle.dumps(session), + } + c.execute('INSERT INTO session VALUES (:value)', values) + conn.commit() + conn.close() + except: + log.error(traceback.format_exc()) + + + def get(self): + """Returns a requests.Session object. + + Gets Session from sqlite3 cache or creates a new Session. + """ + + if not HAS_SQL: + return requests.session() + + try: + conn, c = self.connect() + except: + log.error(traceback.format_exc()) + return requests.session() + + session = None + try: + c.execute('BEGIN IMMEDIATE') + c.execute('SELECT value FROM session LIMIT 1') + row = c.fetchone() + if row is not None: + session = pickle.loads(row[0]) + except: + log.error(traceback.format_exc()) + + try: + conn.close() + except: + log.error(traceback.format_exc()) + + return session if session is not None else requests.session() + + + def delete(self): + """Clears all cached Session objects. + """ + + if not HAS_SQL: + return + try: + conn, c = self.connect() + c.execute('DELETE FROM session') + conn.commit() + conn.close() + except: + log.error(traceback.format_exc())