Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report exceptions in SqliteMultithread. #28

Merged
merged 17 commits into from
Apr 22, 2015
Merged

Report exceptions in SqliteMultithread. #28

merged 17 commits into from
Apr 22, 2015

Conversation

jquast
Copy link
Contributor

@jquast jquast commented Apr 21, 2015

Closes Issue #24

This is a first pass implementation, requesting feedback.

Because of the asynchronous "fire and forget" nature of calling SqliteMultithread.execute(), there is no way for the calling thread to be made aware of an exception in the inner thread.

We handle such exception gracefully, logging to level ERROR the inner exception, the exception only, and the outer stacktrace (the true offending code from the caller's thread).

This might have a slight performance impact.

For the select* family of methods, the inner exception is raised in by calling thread before returning, but for "fire and forget" calls of the execute* family of methods, such as db['key'] = 'value', we may only throw the exception:

  • on commit: somewhat mirroring asyncio's exceptions thrown when accessing the result of a Future.
  • on close or __del__, (which now awaits confirmation before returning, allowing exceptions to be caught).
  • at any subsequent statement after the time that the inner exception occurred.

commit() is made blocking by default (awaiting confirmation), except by implied commit when using by autocommit=True.

For the patient, the work was recorded:

This is a first pass implementation, requesting feedback.

Because of the asynchronous "fire and forget" nature of calling
SqliteMultithread.execute(), there is no way for the calling
thread to be made aware of an exception in the inner thread.

We handle such exception gracefully, logging to level ERROR
the inner exception, the exception only, *and* the outer
stacktrace (the true offending code from the caller's thread).

**This might have a slight performance impact.**

For the ``select*`` family of methods, the inner exception is raised
in by calling thread before returning, but for "fire and forget"
calls of the ``execute*`` family of methods, such as
``db['key'] = 'value'``, we may only throw the exception:

 - on commit: somewhat mirroring asyncio's exceptions thrown
   when accessing the result of a Future.
 - on close or __del__, (which now awaits confirmation
   before returning, allowing exceptions to be caught).
 - at any subsequent statement after the time
   that the inner exception occurred.

commit() is made blocking by default (awaiting confirmation),
except by implied commit when using by autocommit=True.
@jquast jquast mentioned this pull request Apr 21, 2015
@jquast
Copy link
Contributor Author

jquast commented Apr 21, 2015

Example,

$ ipython --debug --pdb
In [1]: import logging, sqlitedict

In [2]: db = sqlitedict.SqliteDict()

In [3]: db[(1, 2)] = 'x'

In [4]: Inner exception:
  File "/usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/threading.py", line 888, in _bootstrap
    self._bootstrap_inner()

  File "/usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/threading.py", line 920, in _bootstrap_inner
    self.run()

  File "/Users/jquast/Code/sqlitedict/sqlitedict.py", line 335, in run
    inner_stack = traceback.extract_stack()


sqlite3.InterfaceError: Error binding parameter 0 - probably unsupported type.


Outer stack:
  File "/Users/jquast/.virtualenvs/sqlitedict34/bin/ipython", line 11, in <module>
    sys.exit(start_ipython())

  File "/Users/jquast/.virtualenvs/sqlitedict34/lib/python3.4/site-packages/IPython/__init__.py", line 120, in start_ipython
    return launch_new_instance(argv=argv, **kwargs)

  File "/Users/jquast/.virtualenvs/sqlitedict34/lib/python3.4/site-packages/IPython/config/application.py", line 574, in launch_instance
    app.start()

  File "/Users/jquast/.virtualenvs/sqlitedict34/lib/python3.4/site-packages/IPython/terminal/ipapp.py", line 371, in start
    self.shell.mainloop()

  File "/Users/jquast/.virtualenvs/sqlitedict34/lib/python3.4/site-packages/IPython/terminal/interactiveshell.py", line 383, in mainloop
    self.interact(display_banner=display_banner)

  File "/Users/jquast/.virtualenvs/sqlitedict34/lib/python3.4/site-packages/IPython/terminal/interactiveshell.py", line 507, in interact
    self.run_cell(source_raw, store_history=True)

  File "/Users/jquast/.virtualenvs/sqlitedict34/lib/python3.4/site-packages/IPython/core/interactiveshell.py", line 2871, in run_cell
    interactivity=interactivity, compiler=compiler, result=result)

  File "/Users/jquast/.virtualenvs/sqlitedict34/lib/python3.4/site-packages/IPython/core/interactiveshell.py", line 2975, in run_ast_nodes
    if self.run_code(code, result):

  File "/Users/jquast/.virtualenvs/sqlitedict34/lib/python3.4/site-packages/IPython/core/interactiveshell.py", line 3035, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)

  File "<ipython-input-3-46080278fee6>", line 1, in <module>
    db[(1, 2)] = 'x'

  File "/Users/jquast/Code/sqlitedict/sqlitedict.py", line 206, in __setitem__
    self.conn.execute(ADD_ITEM, (key, encode(value)))

Exception will be re-raised at next call.

At this point, I don't appear to have a prompt, it returned early, and we're seeing the logging from the inner thread. I do have a prompt, no exception was raised from the main thread. I press return to see a prompt, and execute any subsequent statement, such as db.commit():

In [4]: db.commit()
An exception occurred from a previous statement, view the logging namespace "sqlitedict" for outer stack.
---------------------------------------------------------------------------
InterfaceError                            Traceback (most recent call last)
<ipython-input-4-1411d94ebbaf> in <module>()
----> 1 db.commit()

/Users/jquast/Code/sqlitedict/sqlitedict.py in commit(self, blocking)
    240         """
    241         if self.conn is not None:
--> 242             self.conn.commit(blocking)
    243     sync = commit
    244

/Users/jquast/Code/sqlitedict/sqlitedict.py in commit(self, blocking)
    448             # previous statement are thrown before returning, and that the
    449             # data has actually persisted to disk!
--> 450             self.select_one('--commit--')
    451         else:
    452             # otherwise, we fire and forget as usual.

/Users/jquast/Code/sqlitedict/sqlitedict.py in select_one(self, req, arg)
    438         """Return only the first row of the SELECT, or None if there are no matching rows."""
    439         try:
--> 440             return next(iter(self.select(req, arg)))
    441         except StopIteration:
    442             return None

/Users/jquast/Code/sqlitedict/sqlitedict.py in select(self, req, arg)
    427         """
    428         res = Queue() # results of the select will appear as items in this queue
--> 429         self.execute(req, arg, res)
    430         while True:
    431             rec = res.get()

/Users/jquast/Code/sqlitedict/sqlitedict.py in execute(self, req, arg, res)
    404         `execute` calls are non-blocking: just queue up the request and return immediately.
    405         """
--> 406         self.check_raise_error()
    407
    408         # NOTE: This might be a lot of information to pump into an input

/Users/jquast/Code/sqlitedict/sqlitedict.py in check_raise_error(self)
    397             # retain the original (inner) location of where the exception
    398             # occurred.
--> 399             reraise(e_type, e_value, e_tb)
    400
    401

/Users/jquast/Code/sqlitedict/sqlitedict.py in reraise(tp, value, tb)
     64         if value.__traceback__ is not tb:
     65             raise value.with_traceback(tb)
---> 66         raise value
     67
     68 try:

/Users/jquast/Code/sqlitedict/sqlitedict.py in run(self)
    330             else:
    331                 try:
--> 332                     cursor.execute(req, arg)
    333                 except Exception as err:
    334                     self.exception = (e_type, e_value, e_tb) = sys.exc_info()

InterfaceError: Error binding parameter 0 - probably unsupported type.
> /Users/jquast/Code/sqlitedict/sqlitedict.py(322)run()
    321         while True:
--> 322             req, arg, res, outer_stack = self.reqs.get()
    323             if req == '--close--':

ipdb> req
'REPLACE INTO unnamed (key, value) VALUES (?,?)'
ipdb> arg
((1, 2), <memory at 0x104763e58>)

and here we get the pdb debugger, with the traceback of the original inner exception. We can see the original values of req and arg, for example.

@piskvorky
Copy link
Owner

Oh wow. Lots of changes -- I think you've written more code than the original implementation put together :)

Performance impact -- not a problem. Sqlitedict's purpose is simple API and straightforward "dict-like" compatibility, not HPC.

Question: how does this PR affect backward compatibility? Are there situations (non-error, no exception) where sqlitedict will behave differently now?

@jquast by the way, what do you use sqlitedict for? How did you find it?

I'm adding you to project collaborators, your code is good.

@jquast
Copy link
Contributor Author

jquast commented Apr 21, 2015

Question: how does this PR affect backward compatibility? Are there situations (non-error, no exception) where sqlitedict will behave differently now?

No changes, this could be a minor version bump.

I worked hard towards 100% coverage to make sure of that.

What you use sqlitedict for?

I use it on http://github.com/jquast/x84/ where it stores user records, e-mail-like messages, or even high scores in tetris. I provide a "proxy" that communicates through the existing sub-process IPC event engine, https://github.com/jquast/x84/blob/master/x84/bbs/dbproxy.py#L16 ; I always said we could change sqlitedict out for something else later, but we haven't had to yet, it serves just fine.

I might propose sqlitedict as an alternative for redis in https://github.com/skoczen/will -- I'm annoyed at having to install and run redis-server when i wrote a github api-based bot which did not use or require a database at all. In such cases I could care less about performance, and they only use the k/v part of redis, anyway.

How did you find it?

I was probably googling "Python dict database"

I transitioned from ZODB in the "x84" project some years ago, I have used many k/v db's like mongodb, berkely, or redis before. I wanted something along the DBProxy class I shared above, and users to not have to "Install and configure a database server first!". sqlitedict fit perfectly -- if I hadn't found it, I probably would have wrote it!

I'm adding you to project collaborators, your code is good.

Wow, thanks! Happy to collaborate!

@jquast
Copy link
Contributor Author

jquast commented Apr 21, 2015

On a separate note, I hope to address the data type issue separately and that this code path is not ever used, I may have it emit a "report a bug" kind of message.

Once the data type issue is resolved this bug may not be reproducible except by mocking: It does however make TDD of #25 and #19 much easier when the test-runner doesn't lock up !

piskvorky added a commit that referenced this pull request Apr 22, 2015
@piskvorky piskvorky merged commit d561c9e into piskvorky:master Apr 22, 2015
@piskvorky
Copy link
Owner

Merging. Thanks again Jeff!

I'll also merge Honza's "pickle keys" PR #26 and make a new release. I think these two warrant a major version bump.

@piskvorky
Copy link
Owner

By the way @jquast , could you add a "coverage (coveralls) badge" into the README?
Would be nice to have on the main page / PyPI.

@jquast
Copy link
Contributor Author

jquast commented May 6, 2015

missed your coveralls comment, will do. By the way, all of this work may be reverted in the 2.0 release. We probably should have done a "merge --squash" so that we could have done so easily!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants