You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Async events failing with within Django redirects #4
I'm attempting to fire severals calls to Mixpanel from within a Django view that performs some logic and then redirects to another view. My view looks like this:
What I'm seeing is that the track event never gets called. I'm not sure if the redirect call is somehow canceling the thread. I'm seeing this both in the Django dev server and my staging nginx+uWSGI server (with "enable-threads" turned on). If I flush all the events synchronously, everything works. Here is how I'm setting up my Mixpanel object in settings.py:
from mixpanel import Mixpanel
from mixpanel_async import AsyncBufferedConsumer
mp = Mixpanel(MIXPANEL_TOKEN, consumer=AsyncBufferedConsumer())
I was able to replicate this with a minimal test case. If you create a new Django project and create an application (I called mine 'async'), you can try the following steps.
Append the following to settings.py (inserting your own Mixpanel token):
import mixpanel
from mixpanel import Mixpanel
from mixpanel_async import AsyncBufferedConsumer
MIXPANEL_TOKEN = '<your mixpanel token>'
MIXPANEL = Mixpanel(MIXPANEL_TOKEN, consumer=AsyncBufferedConsumer())
If you start up the dev server and go to localhost:8000 and submit the resulting form, you should see that the "Started trial (test)" event is never posted. If you switch to using the synchronous version of Mixpanel, it should work.
OK, did a little more digging. Observed one thing: if you reduce the it to just a single track event on the form submit, everything works as expected. Can you confirm that you see this as well?
Yes, a single track event works. It's just when there's more than one that
I'm seeing issues. Specifically the alias and track events together seems
to be failing, but I haven't tried other combinations yet.
OK, did a little more digging. Observed one thing: if you reduce the it to
just a single track event on the form submit, everything works as expected.
Can you confirm that you see this as well?
Reply to this email directly or view it on GitHubhttps://github.com//issues/4#issuecomment-39813345
.
I did some more digging and I think the issue is a race condition. After the first event is appended to the buffer, the buffer gets flushed. While this is happening, the second event is appended to the buffer. After the first event is sent, the buffer is cleared. This results in the second event getting dropped without ever being sent.
I'm still trying to confirm that this is the case and then I can start thinking about a fix.
I confirmed that a race condition is the problem and I believe the cleanest way to fix it is to make sure that the mixpanel-python's BufferedConsumer only clears from its buffer what it has already sent. I'll submit a patch to them shortly.
Hey guys, I just responded to the PQ in mixpanel-python, I'm not sure the problem can be solved well there. Would you consider locking access to the buffer?
Locking access to the buffer seems like a bad solution to me — that would essentially mean that the main thread would block while the buffer was being flushed, right?
We could flush the buffer in the main thread before passing it to the async thread?
That sounds like a really good solution, since you're guaranteed only one thread on each end (or if you wanted to get fancy, you could use a Queue and have multiples on each end.) - Cool!
I thought there could only be a single _flush_endpoint running at one time since the thread that fires the flush grabs a lock before it starts? Maybe I'm misunderstanding.
I think it's probably an issue with the thread safety of the _buffers dictionary. Here's what I imagine happens:
An event (e1) gets tracked
Because flush_first is set, e1 instantly gets flushed
While that event is flushing, another event (e2) is added to the buffer
e1 gets sent and the buffer gets cleared, meaning e2 was never sent
The solution I'm proposing would be to essentially copy the buffer, clear it in the main thread, pass the copied buffer to the new thread. With this solution, I imagine this would go something like:
An event (e1) gets tracked
Because flush_first is set, e1 instantly gets flushed
the buffer is copied and cleared
copied buffer is sent to thread to flush
buffer is now empty
While that event is flushing, another event (e2) is added to the buffer
this event goes in the new, empty buffer
whenever the buffer fills up, it is flushed as expected
I do like your approach since it's more general purpose and it would be cleaner to implement in the async library. That said, I don't think the _buffers dictionary thread-safety is the issue, since we're not modifying the dictionary, but the lists contained in them. In the case you mentioned, this is how I think it plays out with my patch:
An event (e1) gets tracked
We note that the buffer size is 1
Because flush_first is set, e1 instantly gets flushed
While that event is flushing, another event (e2) is added to the buffer
Since we saved the buffer size, only the first event is cleared from the buffer
Yeah, both of those solutions do essentially the same thing — lock in the buffer-to-be-flushed at the moment of the flush starting (which fixes the issue of things being added to the buffer mid-flush).
I'm happy with either patch — can you open a PR here that I could take a look at?
My current patch works by monkey-patching the _flush_endpoint from the mixpanel-python library, so it's not very elegant. I'll have to look into implementing it using your method.
Activity
jessepollak commentedon Mar 29, 2014
So, I just tried to replicate this in Flask and I haven't been successful.
When I run that code and hit the
/redirectroute, it buffers then send the events when either the time limit is hit or the max queue size is hit.It'd be awesome if you could provide a small test case in Django where this error occurs so I can attempt to debug.
agmathew commentedon Mar 31, 2014
I was able to replicate this with a minimal test case. If you create a new Django project and create an application (I called mine 'async'), you can try the following steps.
Append the following to settings.py (inserting your own Mixpanel token):
and this to your views.py:
and finally this to your URLs:
If you start up the dev server and go to localhost:8000 and submit the resulting form, you should see that the "Started trial (test)" event is never posted. If you switch to using the synchronous version of Mixpanel, it should work.
jessepollak commentedon Mar 31, 2014
I can confirm that this is an issue. I'll investigate today or tonight.
jessepollak commentedon Apr 8, 2014
OK, did a little more digging. Observed one thing: if you reduce the it to just a single track event on the form submit, everything works as expected. Can you confirm that you see this as well?
agmathew commentedon Apr 8, 2014
Yes, a single track event works. It's just when there's more than one that
I'm seeing issues. Specifically the alias and track events together seems
to be failing, but I haven't tried other combinations yet.
On Tue, Apr 8, 2014 at 1:36 AM, Jesse Pollak notifications@github.comwrote:
agmathew commentedon Apr 8, 2014
I did some more digging and I think the issue is a race condition. After the first event is appended to the buffer, the buffer gets flushed. While this is happening, the second event is appended to the buffer. After the first event is sent, the buffer is cleared. This results in the second event getting dropped without ever being sent.
I'm still trying to confirm that this is the case and then I can start thinking about a fix.
agmathew commentedon Apr 8, 2014
I confirmed that a race condition is the problem and I believe the cleanest way to fix it is to make sure that the mixpanel-python's BufferedConsumer only clears from its buffer what it has already sent. I'll submit a patch to them shortly.
Made BufferedConsumer's _flush_endpoint more thread-safe by making su…
joeatwork commentedon Apr 8, 2014
Hey guys, I just responded to the PQ in mixpanel-python, I'm not sure the problem can be solved well there. Would you consider locking access to the buffer?
jessepollak commentedon Apr 8, 2014
Locking access to the buffer seems like a bad solution to me — that would essentially mean that the main thread would block while the buffer was being flushed, right?
We could flush the buffer in the main thread before passing it to the async thread?
joeatwork commentedon Apr 8, 2014
That sounds like a really good solution, since you're guaranteed only one thread on each end (or if you wanted to get fancy, you could use a Queue and have multiples on each end.) - Cool!
agmathew commentedon Apr 9, 2014
I thought there could only be a single _flush_endpoint running at one time since the thread that fires the flush grabs a lock before it starts? Maybe I'm misunderstanding.
jessepollak commentedon Apr 9, 2014
I think it's probably an issue with the thread safety of the
_buffersdictionary. Here's what I imagine happens:flush_firstis set, e1 instantly gets flushedThe solution I'm proposing would be to essentially copy the buffer, clear it in the main thread, pass the copied buffer to the new thread. With this solution, I imagine this would go something like:
flush_firstis set, e1 instantly gets flushedDoes that make sense?
agmathew commentedon Apr 9, 2014
I do like your approach since it's more general purpose and it would be cleaner to implement in the async library. That said, I don't think the
_buffersdictionary thread-safety is the issue, since we're not modifying the dictionary, but the lists contained in them. In the case you mentioned, this is how I think it plays out with my patch:jessepollak commentedon Apr 14, 2014
Yeah, both of those solutions do essentially the same thing — lock in the buffer-to-be-flushed at the moment of the flush starting (which fixes the issue of things being added to the buffer mid-flush).
I'm happy with either patch — can you open a PR here that I could take a look at?
agmathew commentedon Apr 21, 2014
My current patch works by monkey-patching the
_flush_endpointfrom the mixpanel-python library, so it's not very elegant. I'll have to look into implementing it using your method.jessepollak commentedon Apr 29, 2014
This issue has been fixed in 0.0.3. Sorry about the delayed turnaround time!