sangoma / switchy

async FreeSWITCH cluster control
https://switchy.readthedocs.io/en/latest/
Mozilla Public License 2.0
69 stars 18 forks source link

Ring ready not working on incoming calls (SOLVED) #65

Closed k4ml closed 7 years ago

k4ml commented 7 years ago

Notes: Originally wrote this thinking it's a bug but then managed to figure out myself but I decided to post it through as a notes and question on best approach.

For handling incoming calls, our practice is to ring twice and stay silent for 2 seconds before answering it. We manage to accomplish this via ring_ready and sleep command. So I try to accomplish the same thing with switchy:-

class Phone(object):
    @event_callback('CHANNEL_PARK')
    def on_park(self, sess):
        print(sess)
        logger.info(sess)
        if not sess.is_inbound():
            sess.hangup()

        sess.broadcast('ring_ready::', 'aleg')
        sess.broadcast('sleep::5500', 'aleg')
        sess.answer()
        sess.playback('http://a.clyp.it/b3byghjf.mp3')

s = Service(['127.0.0.1'], auth='mypass')
s.apps.load_app(Phone, app_id='default')
s.run()

That however didn't work, it went immediately to playback. Looking at the log, the command was executed. Comparing it with our working freeswitch instance also look the same command executed inside freeswitch. Only then I realized that we're handling incoming calls through outbound socket in sync mode.

Since switchy is inbound socket and everything asynchronous, it mean sess.answer() was executed immediately, leaving no room for the ring_ready. So I try sleeping in python instead:-

class Phone(object):
    @event_callback('CHANNEL_PARK')
    def on_park(self, sess):
        print(sess)
        logger.info(sess)
        if not sess.is_inbound():
            sess.hangup()

        sess.broadcast('ring_ready::', 'aleg')
        time.sleep(5.5)
        sess.answer()
        sess.playback('http://a.clyp.it/b3byghjf.mp3')

And it work ! Not sure if this is the best way so I appreciate if there's better approach to handle this.

k4ml commented 7 years ago

I'm wondering if there's an event we can listen to know ring_ready is done ...

goodboy commented 7 years ago

Hey @k4ml. Glad you (somewhat) figured it out. So answering in order:

Another approach is to simply delay your answer by scheduling it to happen in the future using FreeSWITCH's internal scheduler. The caveat with this is that you'll probably want to write a separate answer handler:

class Phone(object):
    @event_callback('CHANNEL_PARK')
    def on_park(self, sess):
        print(sess)
        logger.info(sess)
        if not sess.is_inbound():
            sess.hangup()

        sess.broadcast('ring_ready::', 'aleg')
        sess.broadcast("answer::", delay=5.5)

    @event_callback('CHANNEL_ANSWER')
    def on_answer(self, sess):
         if sess.is_inbound():
             sess.playback('http://a.clyp.it/b3byghjf.mp3')

I realize having all these handlers doesn't scale well and makes the code hard to read and understand (which is the motivation for using asyncio and coroutines and which I really want to get done soon). Unfortunately (or fortunately?), switchy was originally built for stress testing and it was easier to just delegate to FreeSWITCH's internal scheduler instead of trying to get performance out of python at the inset; so the callback system was good enough. If you'd like this feature soon I can do my best to hack out something for next week.

Let me know about the py3 vs. 2 thing and we can move forward from there!

k4ml commented 7 years ago

@tgoodlet Fortunately, we already on Python 3 right now. I've been looking at asyncio too lately so it would be great if we can put it into practice.

k4ml commented 7 years ago

@tgoodlet If we can't run blocking operations in the event handler, how do we run blocking function like accessing external HTTP endpoint from the event handler ? Spawn a thread ?

I've also figured out today we can't run execute() from inside thread when handling channel_park. This is using python-ESL lib directly, not switchy.

goodboy commented 7 years ago

@k4ml to do blocking stuff in the event loop we for sure need to add and use asyncio. For our purposes we've been using the non-blocking mod_commands which has worked well enough but of course more generally we need a Python side solution for handling blocking code.

So for right now, yes, you may have to spawn a thread or write your own future mechanism unfortunately.

With regard to execute(), do you need it? You should be able to use broadcast() to accomplish all your app running needs no? Also as I mentioned before, feel free to make a PR to expose execute through switchy if you really need it :)

k4ml commented 7 years ago

I'm not sure yet about execute(). broadcast() seem to work for now.

On the non-blocking issue, I think even spawning a thread won't help since I still need to get the return value of the function running inside thread, which mean it will block again. The whole stack, starting from switchy need to be non-blocking.

goodboy commented 7 years ago

I think even spawning a thread won't help since I still need to get the return value of the function running inside thread, which mean it will block again. The whole stack, starting from switchy need to be non-blocking.

@k4ml yes agreed it does and hence asyncio. In the interim you can use something like requests-futures if you want to do the request non-blocking in the event loop.

I will push hard on trying to get asyncio support going on a development branch over the the next week or so.

goodboy commented 7 years ago

@k4ml since this was a question and is I'm closing. I've started asyncio progress on my own fork and I'll track that using #54.