среда, 18 января 2012 г.

SamaraPy

Зависть великая штука. Побывав на PyConUA я заразился чудной атмосферой живого общения с едино и разномышленниками. Я не уверен, что получится в Самаре найти достаточное число участников для SamaraPy, который более будет похож поначалу на KievPy, но я попробую.

Подробности будут тут http://vk.com/club34256705

В конце концов оказался же в этом городе Я!

FreeSWITCh, love over

Я больше не занимаюсь FreeSWITCH по нескольким причинам. Первая - я теперь не сетевой администратор с горкой АТС, и не работаю в VoIP стартапе.

Вторая - считаю разработчиков FreeSWITCH-а неадекватными по нескольким пунктам.

1. Прием патчей. Когда у нас глюкало видео я искал проблему и нашел ее. Создал патч и отправил в жиру. После месяца боданий патч был принят с формулировкой "а предыдущий пацанчик говорил что все работает отлично, но вы достали, хрен с вами". Многообщающе.

2. Использование DMCA для блокировки репозитория с кодом g729 кодека. Нормальные люди вначале пишут и говорят - у вас тут мои копирайты, вы поправьте. В общем мне их мотивировка неясна - их права никак не были нарушены, но шило в попе творит чудеса.

В общем как для профессионального разработчика для меня FreeSWITCH это один сплошной дурдом. Без причин с ним возиться я с ним возиться никакого желания не имею.

вторник, 23 февраля 2010 г.

Django в неблокирующем стиле, или в погоне за Священным Граалем

Было опубликовано на Хабре.

Присказка

При чтении о Twisted, Tornado, Node.js, у многих python-программистов возникает вопрос - "а вот если взять, и переписать Django в неблокирующем стиле?". Обычный ответ на этот вопрос - нет, не дождетесь. И правда, чтобы переписать целый фреймворк в макаронно-колбечном стиле, надо очень много сил, и большой заряд энтузиазма. Писать с колбеками, очень сомнительное удовольствие.

Так бы и было, но как я писал в своей прошлой заметке, есть в python-мире greenlet-ы, которые легким движением руки помогают скрыть от программиста все эти асинхронные моменты, до определенной степени. В момент написания той заметки, к мысли заставить работать Django в неблокирующем режиме, я относился весьма скептически - ну в самом деле, кто за это возьмется?



Суть

А на самом деле все не так страшно, следите за руками - мы создаем WSGI ресурс в Twisted, начинаем слушать HTTP-запросы; каждый запрос мы отдаем в WSGI-обработчик Django, оборачивая его в гринлет; любые запросы, которые можно преобразовать в неблокирующие, передаем Twisted, который пнет наш гринлет в момент готовности данных; готово!


А что же у нас есть такого, что можно обратить в неблокирующий режим в первую очередь? В Django основным таким местом выступает база данных. Можно ли с ней общаться асинхронно? Абсолютно верно, но нужен специальный драйвер.

С давних пор в Twisted есть такой - PGAsync. У него есть серьезные проблемы, он давно заброшен, но кто нам мешает подхватить упавшее знамя? Я сделал такую попытку, на данный момент драйвер работает стабильнее оригинала, но работа еще не закончена. Тем не менее, асинхронную БД для Django мы получить можем, и это будет Postgresql (ох, простите, пользователи Mysql, но кто вам мешает написать свой?). Драйвер используется в текущем проекте, разрабатываемом Imarto Networks http://imarto.net, и было решено выложить его для свободного использования. Я выложил его раньше готовности, только для демонстрации асинхронного Django, будьте снисходительны.

Чтобы использовать его в Django, нам надо написать свой БД-бэкенд. Я наворотил что-то из бэкенда для psycopg2, обернув обращения к драйверу БД в функции передачи управления главному гринлету, то есть основному потоку программы. Стандартный wsgi-ресурс из twisted.web пришлось тоже немного переделать, ведь он теперь должен кидать запросы на обработку не в пул потоков, а в гринлеты.

Как пощупать

Исходники можно получить тут, там же тестовый django-проект - http://bitbucket.org/deepwalker/tx_green/
Исходники драйвера к БД - http://bitbucket.org/deepwalker/tx-postgresql/

Для использования надо разместить pgasync, green_wsgi.py, tx_green.py, tx_postgresql в место, где интерпретатор сможет их найти. Затем в каталоге test_green_dj настраиваем параметры подключения к БД, ./manage.py syncdb, twistd -n - y server.py. Все, по адресу http://127.0.0.1:8001 встречаем наш тестовый проект.

Про тесты

Тесты, которые я проводил, относились в основном к проверке работы, прироста производительности я не измерял. Если кто-то потратит свое время на это доброе дело, буду благодарен, если поделитесь результатами.

суббота, 9 января 2010 г.

Озеленение Twisted

Как обычно, в праздники, в свободное время от затирки плитки и прочих прибиваний плинтуса, меня захватила очередная идея из цикла "попробовать". На хабре проскочила статья о Pyrant. Первой итерацией я взял и переделал основную часть протокола на Twisted - http://github.com/Deepwalker/tx-tokyo . И все было хорошо, наступление шло по всем фронтам, но тут я перешел к питоничной части pyrant, и понял что сделать yield a[megakey]='mega data string', вообще говоря невозможно. Это было очень печально, ведь в статье меня зацепило именно легкое обращение с данными в питоничной форме. Что же делать, Пух, спросил я себя? И вспомнил о greenlet-ах.

Помучавшись некоторое время, я переписал defer.inlineCallbacks на использование greenlet-ов и смог написать такой код:
from twisted.internet import defer, protocol, reactor
from tx_green import inlineCallbacks
from greentokyo import Tyrant

@inlineCallbacks
def test_proto():
    t = Tyrant()
    print t.get_stats()

    t['kuku'] = 'Green Tyrant!'
    print t['kuku']

    reactor.stop()

if __name__=='__main__':
    test_proto()
    reactor.run()


Удобство в том, что greenlet-ы имеют важное отличие от генераторов - они не ограничены одной функцией. То есть при вызове t['kuku'], на самом деле вызывается t.__getitem__, внутри которого мы и делаем вызов switch() для возврата управления основному циклу на время, пока мы ожидаем данные от сервера. Если бы мы попробовали сделать внутри t.__getitem__ yield, то мы бы просто сделали из него генератор, не получив никакой пользы.

Неудобство состоит в том, что надо помнить обо всей этой начинке - будет плохо если вы будете ждать данные, а вместо этого вам упадет просто Deferred. И будет еще хуже, если вы забудете вернуть управление основному потоку - остановите все приложение. И еще в этом варианте не светит "not twist your brain", потому что надо помнить и использовать возможности Twisted.

Стоит обратить внимание, что в приведенном коде нет ничего, явно указывающего на возврат управления - вызовы greenlet.switch() скрываются в реализации питоничного Tyrant. То есть он заточен под эту реализацию. Если бы это было не так, то нам бы пришлось явно вызывать функцию wait из tx_green. Именно этот момент меня беспокоит - можно создать чуть ли не джангу, и обращения к БД через ORM будут работать также скрытно вызывая wait, но если забыться, то потенциально можно породить трудноуловимую ошибку, или новичок, как обычно не прочитав толком документацию, начнет запрашивать с какого-либо сервера странички через urllib и будет долго удивляться, почему сервер подтормаживает.

Реализация расположена в моей песочнице.

Вот такой небольшой пост. Как всегда ожидаю приятного обсуждения, и надеюсь, что будут хорошие предложения по коду, как было в прошлой нашей встрече, со мной в амплуа постописателя. Спасибо за внимание.

воскресенье, 13 декабря 2009 г.

Twisted в действии — memcache на python

Преамбула

В связи с выходными потратил немного времени на реализацию сервера Memcache с использованием python-фреймворка Twisted. В итоге я получил быстродействие в два раза более низкое, что я не считаю очень критичным, а также возможность реализовать парочку расширений оригинального протокола. Также возможны оптимизации, которые еще улучшат быстродействие.
Протокол не был реализован полностью - есть еще моменты над которыми можно поработать, но стандартные set/get вполне работоспособны и готовы к использованию.

Средства

Для хранения кеша используем базовый класс dict. Как вы догадываетесь, реализация dict в python быстра, этот базовый тип используется в python настолько активно, что его не оставили без детальной оптимизации. Таким образом, мы автоматом имеем структуру для хранения кеша в памяти. Осталось реализовать протокол memcache, для предоставления доступа к dict другим программам.

Для реализации сервера используем Twisted. Есть множество вариаций неблокирующего IO для python на сегодня, но Twisted это уже классика, и имеет в своем арсенале достаточно средств для легкого решения подобных задач.



Реализация сетевого протокола

Как реализуют протоколы? Первым делом вам конечно же нужно найти описание протокола. Я нашел его здесь - http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt

После прочтения протокола становится понятно, что от клиента мы получим одну или две строки, причем первую строку мы можем смело разбивать на элементы по пробелам. Вторая строка используется в командах, которые передают серверу данные - set, add, replace и т.п. Если вам хочется подробнее вникнуть в статью, то отправлю вас почитать описание самостоятельно, цели выложить его перевод сюда не было.

Вооруженные этим знанием, смотрим, что нам может предложить Twisted для решения этой задачи, и сразу находим LineOnlyReceiver - протокол из базовой поставки Twisted, который работает только с протоколами, обменивающимися строками, то есть то, что надо.
                                                                                                                  
class MemcacheProtocol(LineOnlyReceiver):                                                                                    
    """                                                                                                                      
    Реализует базис протокола - прием сообщений от клиента                                                                   
    и отдачу результата.                                                                                                     
    """                                                                                             
    def lineReceived(self,line):            
        debug(repr(line))                   
        if not 'parameters' in self.instruction:
            parameters = line.split(' ')        
            debug("Got new command "+parameters[0])
            self.instruction['parameters']=parameters

            # Если данных не ожидается, то к исполнению
            if parameters[0] in Cache.oneline_commands:
                self.process()                         
        else:                                          
            # Получены данные к двухстрочной команде, к исполнению
            debug("Got data "+line)                               
            self.instruction['data']=line                         
            self.process()                                        

    def process(self):
        # Cache.call возвращает генератор
        for line in Cache.call(self.instruction):
            # И мы отсылаем все что он нагенерирует отдельными строками
            debug("Send line "+line)                                   
            self.sendLine(line)                                        
        # Готовы к дальнейшим инструкциям, насяльника!                 
        self.instruction={}                                            

    def connectionMade(self):
        debug("Connected!")  
        self.instruction={}  

Как видно из кода, для собственно работы используется Cache. Это синглетон, по сути просто класс, методы которого обернуты декоратором @classmethod. Вызов Cache.call должен вернуть генератор, которые будет возвращать строки, которые, в свою очередь, наша реализация протокола, будет отдавать клиенту.

Разбираем запрос от клиента

Первая строка это команда и параметры, разделенные пробелами, поэтому используем строковый метод split, и на выходе получаем список. Далее его надо разобрать на составляющие, перед тем как с данными начнет работать команда. Я использую класс, так как мне нравится перспектива обращаться к параметрам, указывая их через точку. Приведенный ниже код уже требует прочтения описания протокола, а для ленивых пара наводящих строк:
Команды записи данных:                                                                                                       
     [noreply]\r\n                                                                 
cas      [noreply]\r\n                                                               

Получение данных:
get *\r\n   
gets *\r\n  
delete \r\n 

Ну и тому подобное.
Реализация разбора:


class Instruction(object):
    def __init__(self, i):
        p = i['parameters']
        self.cmd = p.pop(0)

        # Проверяем noreply
        if p[-1]=='noreply':
            self.reply=False
            # Выкидываем его
            p.pop(-1)       
        else:               
            self.reply=True 

        if self.cmd in Cache.storage_commands:
            # Если CAS то есть еще один параметр (т.е. особый случай)
            if self.cmd == "cas":                                    
                self.unique = p.pop(-1)                              

            # Теперь все параметры однозначны, но мы хотим расширить протокол,
            # потому все не так просто, как dict(zip())                       
            self.bytes = p.pop(-1)                                            
            self.exptime = p.pop(-1)                                          
            self.flags = p.pop(-1)                                            
            self.data = i.get('data',None)                                    

        # incr, decr
        elif self.cmd in ["incr","decr"]:
            self.change_value = p.pop(-1)

        self.keys = p

    def __str__(self):
        return str(self.__dict__)

Реализация хранения кеша и работы с ним

Протокол мною сразу же был расширен, а именно есть возможность работы с вложенными данными. Кеш переделан в древовидный, и все операции, которые по стандарту указывают один ключ, могут указывать список ключей, разделенных пробелами. Впрочем от этого легко избавиться, но тогда будет совсем неясен смысл работы.

В качестве единицы хранения реализован класс Entry, в котором содержится словарь(childs типа dict) с дочерними экземплярами Entry. Более того - верхней точкой в иерархии также является экземпляр класса Entry.

Здесь же я приведу фрагмент синглетона Cache:


class Cache(object):
    # consts
    storage_commands = ["set", "add", "replace", "append", "prepend","cas"]
    oneline_commands = ["get", "gets","getn", "delete", "incr", "decr", "stats"]

    # cache storage
    data = Entry(0,0,0)

    # cache operations
    @classmethod
    def call(cls, instruction):
        i = Instruction(instruction)
        debug(i)
        command = getattr(cls,i.cmd)
        return command(i)

    @classmethod
    def set(cls, i):
        "set, поддержка вложенных ключей"
        parent = cls.data.get_child(i.keys[:-1])
        if parent:
            parent.set_child(i.keys[-1], Entry(i.data,i.flags,i.exptime))
            yield "STORED"
        else:
            yield "NOT_STORED"

    @classmethod
    def get(cls, i):
        "get, не обрабатывает вложенные ключи"
        for key in i.keys:
            entry = cls.data.get_child([key])
            if entry:
                yield ' '.join(( "VALUE", key, entry.flags, str(len(entry.data)) ))
                yield entry.data
        yield "END"

Код Entry и всего остального смотрим тут - http://github.com/Deepwalker/tx-cache/blob/master/mck.py

суббота, 7 ноября 2009 г.

FreeSWITCH, сервис эха.

Обычно эхо делается так:

  • приветствуем

  • бибикаем

  • портим текст записываем в файл

  • бибикаем

  • проигрываем файл

  • откладываем скрипку прощаемся



Во FreeSWITCH есть более изящный вариант - приложение delay_echo. Единственный параметр это длительность задержки (буфера) перед воспроизведением звука обратно. Буфер размещается в памяти, диск не дергается, система не упирается в диск, но любит память. По моему восхитительно, учитывая еще и необходимость файлы стирать.


 1     <extension name="delay_echo">
 2       <condition field="destination_number" expression="^echo123$">
 3         <action application="answer"/>
 4         <action application="sleep" data="1000"/>
 5         <action application="playback" data="/opt/freeswitch/sounds/ru/RU/elena/voicemail/8000/vm-greeting.wav"/>
 6         <action application="playback" data="/opt/freeswitch/sounds/ru/RU/elena/voicemail/8000/vm-record_message.wav"/>
 7         <action application="sleep" data="1000"/>
 8         <action application="gentones" data="%(200,0,800)"/>
 9         <!--action application="sched_broadcast" data="+10 gentones::%(200,0,800)"/-->
10         <action application="sched_transfer" data="+20 after_echo XML public"/>
11         <action application="delay_echo" data="10000"/>
12       </condition>
13     </extension>
14
15     <extension name="delay_echo_next">
16       <condition field="destination_number" expression="after_echo">
17         <action application="sleep" data="1000"/>
18         <action application="playback" data="/opt/freeswitch/sounds/ru/RU/elena/voicemail/8000/vm-goodbye.wav"/>
19         <action application="sleep" data="1000"/>
20         <action application="hangup"/>
21       </condition>
22     </extension>
23

среда, 28 октября 2009 г.

Регистратор SIP на twisted.

Питонисты в курсе, что есть такая чудо библиотека для работы с сетевыми протоколами как Twisted. Она немного сложна поначалу, но после просветления становится совершенно необходимым инструментом.
В стандартной поставке с ней идет множество уже реализованных протоколов - IMAP, XMPP, HTTP (в сочетании с обработкой URL через джанго и какой-нибудь библиотекой шаблонов весьма полезный инструмент) и т.д. Количество же протоколов, которые реализованы на/для Twisted не поддается подсчету. Таким образом Tornado может спать спокойно - сравнивать его с Twisted вообще некорректно.

Среди стандартных протоколов нашел SIP - последний раз обновлялся 3 года назад, есть проект VoIP телефона shtoom, который также заглох. Но тем не менее вот вам регистратор на Twisted:

#!/usr/bin/env python
# coding: utf-8

from twisted.application import internet, service

import sip

DOMAIN='192.168.9.5'

application = service.Application("JuzzCallBack")

sip.RegisterProxy.registry = sip.InMemoryRegistry(DOMAIN)
sip.RegisterProxy.locator = sip.RegisterProxy.registry
sip.RegisterProxy.debug = True
proxy = internet.UDPServer(5060,sip.RegisterProxy())

proxy.setServiceParent(application)

И что удивительнее всего - он работает. На досуге хочется попробовать сделать простейший SIP-телефон, будет интересно. Назову его Shtoom ressurection, он не будет обладать даже функциями предшественника, и сдохнет на версии -0.1, ожидайте!