2014-10-16

Python: how to start independent process

Задача: запустить полностью независимый процесс из Python. Попробуем стартовать в Linux простой HTTP сервер и отправить его в свободное плавание:

import subprocess
subprocess.Popen(['python', '-m', 'SimpleHTTPServer', '5000'], close_fds=True)
Однако параметр "close_fds" в Windows не работает: "Note that on Windows, you cannot set close_fds to true". Как вариант, можно использовать стандартные компоненты Windows, cmd.exe и команду start:

import subprocess
subprocess.Popen([‘C:\Windows\System32\cmd.exe’, ‘/C’, ’start’, ‘C:\Python27\Python.exe’, ‘-m’, ’SimpleHTTPServer’, ‘5000’])
Это создаст новое окно cmd.exe, где будет запущен http-сервер. Что и требовалось.

2014-09-16

Работа с Azure SQL из PHP в Ubuntu

Приведу пример, как это делается в Ubuntu. Установим нужные пакеты

sudo apt-get install php5-cli php5-sybase freetds*

Изменим файл концигурации FreeTDS /etc/freetds/freetds.conf, добавив секцию

[xxxserver.database.windows.net]
    host = xxxserver.database.windows.net
    port = 1433
    tds version = 8.0

Короткий кусочек кода на PHP5

<?php
$server = 'xxxserver';
$dbname = 'xxxdbname';
$user = "xxxuser@{$server}";
$passwd = 'xxxpassword';

$p = new PDO("dblib:host={$server}.database.windows.net;dbname={$dbname}", $user, $passwd);
foreach($p->query('SELECT col FROM table') as $row){
        echo $row['term'];
}
?>

Это очень актуально для Yii-фреймворка.

2014-08-13

Python: how to stop process correctly

Одной из задач, с которой встречаются разработчики многопоточных/многопроцессных приложений, является их корректная остановка. Особенно это касается таких потоков/процессов, в которых осуществляется бесконечная блокирующая операция, типа цикла "while True" или чтения из некоего генератора.

while True:
    # do something
С потоками проще, потому что им можно передать ссылку на объект, который можно проверять при каждой итерации и останавливать при соблюдении условий.

import time
from threading import Thread

def in_thread(con):
    while con[0]:
        # ... doing something
        pass

con = [True]
t = Thread(target=in_thread, args=(con,))
t.start()

time.sleep(3)
# Stop the thread
con[0] = False
t.join()
С процессами несколько иначе, они более изолированные, и нужно использовать объекты синхронизации. Очень удобный способ мне подсказал коллега, с использованием Lock.

import time
from threading import Thread
from multiprocessing import Process, Lock

def in_process(lock):

    def in_thread(con):
        while con[0]:
            pass
            # ... doing something

    con = [True]
    t = Thread(target=in_thread, args=(con,))
    t.daemon = True
    t.start()

    # Lock acquired in parent process, child process waits, but thread already ran!
    lock.acquire()

    # Polite way
    con[0] = False
    t.join()

    # Way for impatient
    # import os
    # os._exit(0)


lock = Lock()
lock.acquire()
Process(target=in_process, args=(lock,)).start()

time.sleep(3)
# ... process in action ...

# Release lock, child process going to be finished
lock.release()

2014-07-07

MySQL: UNIQUE KEY + unicode

В прошлой заметке я писал про хранение 4-байтных символов в MySQL и упомянул UTF8MB4, как расширенный вариант UTF8. Оно решает ту задачу, и можно было бы объявить про универсальный и лучший на свете collation, но сегодня я столкнулся с некоторым исключением, которое внесло ложечку дегтя в мои впечатления.

При создании уникального индекса на некое поле с юникодными данными получил ошибку о дублирующихся данных. А ведь их там нет. Проверка показала, что для UTF8MB4 строки "ame", "âme", "Amè", "AMÉ", "ÁME", "Ãme" и "ÂMÈ" одинаковые. Это говорит и SELECT.

Так вот, интересно то, что установив collation в UTF8_BIN я получил нужный результат. Понятное дело, что в этом случае сравнение идет побайтово и всё такое. Фокус в том, что для UTF8MB4_BIN такого эффекта не получаем! Разработчики схитрили или упустили этот момент? Кто знает..

2014-07-01

MySQL: хранение 4-байтных символов юникода

По умолчанию в полях VARCHAR при установленном collation UTF8 не могут храниться строки с символами юникода, состоящими из более чем 3 байт. А такие символы существуют, хоть и не очень часто встречаются. Например "\xf0\x9f\x99\x8e".

Решение в том, чтобы использовать модернизированный collation, UTF8MB4. Просто установите такой на нужной таблице и можно работать. Возможно перед вставкой стоит выполнить команду "SET NAMES UTF8MB4"

P.S. Для поисковиков: "Warning: Incorrect string value"

2014-06-25

MSSQL: псевдопартиционирование

Допустим, у нас есть (а у нас есть) таблица огромных размеров, с огромными индексами и вообще всё у неё огромное. Скажем, это записи из твиттера. Из этой таблицы часто делаются выборки по полю DATETIME. И, хотя на этом поле есть индекс, и несомненно есть Primary Key (вобщем все как надо), доступ к данным осуществляется дольше, чем хотелось бы: для использования индекс загружается в оперативную память, а он, как вы помните, огромный.

Здравые размышления приводят к выводу - нужно табличку поделить, чтобы индексы стали поменьше и помещались в память без проблем. В тоже время шардить табличку не хочется, периодически она нужна целиком. Ну и расширяться пока возможностей нет.

Вариантом временного решения может стать VIEW. Мы разделим таблицу на несколько поменьше, которые будут содержать данные в некоторых заранее известных границах. Создадим VIEW, в котором укажем на эти пределы:

CREATE VIEW table AS
SELECT * FROM table1 WHERE created_at BETWEEN '2014-02-01 00:00:00.000' AND '2014-03-01 00:00:00.000'
UNION ALL
SELECT * FROM table2 WHERE created_at BETWEEN '2014-03-01 00:00:00.000' AND '2014-04-01 00:00:00.000'

Фокус в том, что если запросить данные с указанием условия, присутствующего во VIEW, таблица, не удовлетворяющая этому условию затронута не будет вовсе. Например:

SELECT * FROM table WHERE created_at > '2014-03-01 00:00:00.000'

заденет только вторую таблицу, соответственно поднимет только ее индекс. Как-то так.

2014-06-20

MongoDB: шардинг по быстрому

Шардинг, как известно, позволяет раскидать данные по нескольким серверам БД, распределяя их согласно некоторым условиям. В MongoDB это бесплатно, так сказать all inclusive, к тому же невероятно просто.

Запустим несколько шардов, например на разных портах (или серверах, если есть). Я это делаю в нескольких терминалах, чтобы видеть, что происходит. Но можно добавить ключ --fork для отвязки от терминала.

$ mongod --dbpath db1 --port 27001 --shardsvr
$ mongod --dbpath db2 --port 27002 --shardsvr
$ mongod --dbpath db3 --port 27003 --shardsvr

Запустим шард-сервер и назначим ему один из шардов (на порту 27001) в помощь - там он будет хранить информацию о том, куда что записал:

$ mongos --configdb 127.0.0.1:27001

Цепляемся к шард-серверу, подключаем шарды, указываем что шардить и по какому ключу:

$ mongo
> use admin
> db.runCommand({addshard: "127.0.0.1:27002"})
> db.runCommand({addshard: "127.0.0.1:27003"})
> db.runCommand({enablesharding: "social"})
> db.runCommand({shardcollection: "social.tweets", key: {id:1}})

MongoDB: репликация по быстрому

Вдохновленный этой краткой заметкой решил сделать себе мемо в блоге, дополнив некоторыми замечаниями. Итак, репликация в несколько строк:
Создаем каталоги для данных:

mkdir -p db1 db2 db3

В терминалах запустим несколько реплик в одном наборе "myset", на своих портах и со своими каталогами для данных:

$ mongod --port 27001 --dbpath db1 --replSet myset
$ mongod --port 27002 --dbpath db2 --replSet myset
$ mongod --port 27003 --dbpath db3 --replSet myset

Подключаемся к одной из реплик, делаем ее "мастером", подключаем к ней остальные реплики и проверяем:

$ mongo --port 27001
> rs.initiate()
> rs.add("myhost:27002")
> rs.add("myhost:27003")
> use test
> db.test.insert({id:1})
> db.test.find()

где "myhost" - имя хоста, которое можно увидеть в выводе команды rs.status()

Примечательно и прекрасно, что реплики могут находиться на разных машинах, увеличивая бутылочное горлышко I/O. В своих экспериментах я размещал реплики на PC с Windows 7 на борту, на Macbook и на Ubuntu в VirtualBox.

Важно, чтобы машина с репликой могла однозначно интерпретировать хост "мастера" в IP. На макбуке мне для этого пришлось добавить запись в /etc/hosts:

myhost 192.168.10.130

2014-03-31

Запуск скрипта при старте и выключении CentOS Linux

Задача: в CentOS запускать некий скрипт "test.py" при старте и выключении компьютера от имени некоего пользователя. Для ее решения напишем init-скрипт, например "status_sender" и поместим его в каталог /etc/rc.d/init.d/

Содержимое init-скрипта:


#!/bin/bash

# chkconfig: 35 99 01
# description: Send system status
# processname: status_sender

. /etc/rc.d/init.d/functions

username="username"
lock_file=/var/lock/subsys/status_sender
log_datetime=`date -u +"%F %T"`

start(){
    touch "$lock_file"    
    daemon --user=$username /usr/local/bin/python2.7 "/home/$username/test.py" "Service started at $log_datetime UTC"  &>/dev/null & 
}

stop(){
    rm -f "$lock_file"
    daemon --user=$username /usr/local/bin/python2.7 "/home/$username/test.py" "Service stopped at $log_datetime UTC" &>/dev/null &
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    *)
        echo $"Usage: $0 {start|stop}"
        exit 2
esac
exit 0

Скрипт готов, запускаем создание символических ссылок в соответствующих настройкам каталогах. Это можно сделать и вручную, но зачем?


chkconfig --add status_sender

Проверить результат можно командой


chkconfig --list status_sender

Настройки того, куда и с какими именами будут создаваться симлинки, задаются в строке "chkconfig: 35 99 01". Это означает, что скрипт будет актуален для запуска системы с уровнем 3 и 5, причем старт осуществлять после загрузки всех сервисов (99), а останавливать в первую очередь (01).

Запуск от имени пользователя осуществляется с помощью конструкции


daemon --user=$username

где "$username" - имя пользователя в системе, а "&>/dev/null" предотвращает вывод скрипта в консоль. Команда "daemon" и другие служебные функции находятся в файле "/etc/rc.d/init.d/functions", поэтому его мы импортируем в самом начале.

"lock_file" - служебный файл, который позволяет системе определять, запущен ли сервис и нужно ли его останавливать, запуская скрипт с параметром "stop" при выключении или презагрузке. Если его не создавать, то будет осуществляться только запуск скрипта, а если не удалять - то только остановка. Для системных сервисов такой файл создается автоматически, для пользовательских же его необходимо создать самостоятельно.

"log_datetime" - это строка даты и времени в UTC для передачи python-скрипту.

2014-03-05

Загрузка файла на Amazon EC2 с помощью Python

Есть у меня задачка, с определенной периодичностью заливать через FTP на имеющийся хостинг Amazon EC2 несколько файлов. Некоторое время я делал это тупо через FTP-клиент. Открыл, соединился, нашел нужную папку, закинул файл, закрыл клиент... Вобщем, как обычно.
Но настоящий программист - ленивый программист. Он всегда ищет, как бы автоматизировать рутинные процессы. В решении этой задачи я использую Python.
В составе поставки есть модуль для работы с FTP: ftplib. Однако в нем не нашлось методов для работы с SFTP, использующим для авторизации файл ключа *.pem. Поэтому я использовал pysftp
Использование выглядит простым, как штанга. Пример все пояснит.

srv = pysftp.Connection(host='sitename.org', username='username', private_key='/path/to/file.pem')
srv.execute('mkdir /server/path')
srv.put('/path/to/local/file.html', '/server/path/file.html')
srv.close()

А теперь начнем делать чудеса, доступные пользователям Mac. Создаем каталог, на который навешиваем действие, созданное в Automator. Событие добавления нового файла будет вызывать наш скрипт, передавая ему путь к файлу. После загрузки будет выведено сообщение во всплывающем окне.

2014-01-17

MSSQL UPSERT, или ON DUPLICATE KEY UPDATE по мелкомягкому

Сегодня хочу немного рассказать о том, как реализовать в MSSQL возможность, встроенную в MySQL, позволяющую обновить запись при вставке, если она уже существует. Одним словом, так называемый UPSERT.

В MySQL это выглядит так:

INSERT INTO(id, text) VALUES(12, 'Current text')
ON DUPLICATE KEY UPDATE text='New text';

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

Первый вариант - это просто проверять существование записи перед добавлением. Гром и молния, кэп, как я сам не догадался! Да и не нужен бы мне тогда был upsert, правда? Это совсем не то, что я ищу, отпадает.

Второй вариант, который настойчиво предлагают советчики, это использовать MERGE. Его смысл - перенос данных из одной таблицы (запроса) в другую с проверкой совпадений значений некоторых полей. Что-ж, вроде годная штука, давайте попробуем. Нижеописанную задачу во многих случаях можно решить группировкой исходных данных, но не во всех. Нам нужно при совпадении значений полей увеличивать значение поля в таблице, куда переносятся данные, вместо копирования новой строки.

-- Создадим переменную типа таблицы с составным уникальным индексом
DECLARE @temp TABLE(word NVARCHAR(512), term_id INT, word_count SMALLINT, UNIQUE(word, term_id))

MERGE @temp as TMP
-- Источник данных, им может быть и таблица, и выборка
USING (SELECT word, term_id FROM table1) as TWT
-- Условия совпадения, при которых будем делать Update
ON TMP.word = TWT.word AND TMP.term_id = TWT.term_id
WHEN MATCHED THEN
    -- Нашли совпадение, изменим значение поля счетчика
    UPDATE SET TMP.word_count += 1
WHEN NOT MATCHED THEN
    -- Совпадения не найдено, добавляем
    INSERT (word, term_id word_count) VALUES (TWT.word, TWT.term_id, 1)
;

Сюрприз! При добавлении строк проверка осуществляется только в начале, а строки, которые будут добавлены потом на наличие совпадений не проверяются. Это легко можно проверить, добавив вручную в таблицу @temp пару строк, которые вскоре будут туда добавляться запросом. Так что для нашего случая это не годится.

Наконец третий, рабочий, но медленный вариант. Мы будем использовать конструкцию BEGIN TRY. Работаем с той-же таблицей @temp, что в предыдущем примере. Мы будем проходит курсором по выборке из исходной таблице и пробовать добавлять в @temp, и обновлять данные при ошибке уникального ключа.

-- Переменные, куда будем складывать данные из курсора
DECLARE @word NVARCHAR(512)
DECLARE @term_id INT
-- Объявляем и открываем курсор с данными
DECLARE words SCROLL CURSOR FOR SELECT word, term_id FROM table1
OPEN words
-- Проходим по курсору
FETCH NEXT FROM words INTO @word, @term_id WHILE @@FETCH_STATUS = 0
BEGIN
    BEGIN TRY
        -- Пробуем вставить строку
        INSERT INTO @temp VALUES(@word, @term_id, 1)
    END TRY
    BEGIN CATCH
        -- При ошибке делаем update
        UPDATE @temp SET word_count = word_count + 1
        WHERE word = @word AND term_id =  @term_id
    END CATCH
    -- Берем данные из следующей строки
    FETCH NEXT FROM words INTO @word, @term_id
END
-- Закрываем и освобождаем курсор
CLOSE words
DEALLOCATE words

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

2014-01-09

Python и чтение списков

Не так давно меня спросили - как бы ты прочитал список в Python в обратном порядке? Я предположил, что самым естественным образом будет просто выборка из списка по индексу от максимального (равного len(x) - 1) в порядке уменьшения. Сегодня я решил проверить варианты с подсчетом производительности. Python 2.7.4 x64, 8 Гб RAM, AMD Phenom II X4 960T, 3.00 GHz.

Итак, создаем список из ста миллионов записей и проходим по нему разными способами.


big_list = range(100000000)
big_list_len = len(big_list)

# Прямой перебор, самый быстрый и естественный: 10.3 sec
for item in big_list:
    a = item

# Перебор с выборкой по индексу в прямом порядке: 12.8 sec
for num in xrange(big_list_len):
    a = big_list[num]

# А теперь в обратном порядке: 12.7 sec
for num in xrange(big_list_len-1, 0, -1):
    a = big_list[num]

# И наконец, используем встроенную функцию reversed: 11.9 sec
for item in reversed(big_list):
    a = item

Выводы очевидны. Самый быстрый способ взять данные в обратном порядке - использовать reversed и прямой перебор. Выборка по индексу - не намного, но медленнее.