пятница, 28 января 2011 г.

Изменение поведения транзакций в SQLite 3

Транзакция в SQLite может быть явно начата SQL-командой BEGIN TRANSACTION или неявно начинаться вместе с подключением к базе данных (второе поведение бывает, если подключение произведено не в режиме auto_commit. CLI клиент sqlite3 всегда работает в режиме auto_commit, библиотеки sqlite3 в различных языках программирования могут вести себя по-разному - напр. в питоновском модуле sqlite3 (он же pysqlite2.dbapi2 в python 2.4) auto_commit по умолчанию выключен и может быть включен необязательным аргументом isolation_level метода connect(), установленным в None.
Тут осмелюсь заметить, что - если многопользовательский доступ имеет для вас значение - я бы предложил так и делать - ибо при запуске транзакций явно SQL-командами  BEGIN TRANSACTION они ведут себя так, как описано ниже, а при использовании же других значений isolation_level, кроме None - т.е. при неявном старте транзакций - их поведение показалось мне весьма своеобразным. Судя по всему, неявная транзакция запускается первой командой изменения данных, а при таком поведении разница между DEFERRED и IMMEDIATE (подробнее см. ниже) делается не наблюдаемой. Остается два вида транзакций - EXCLUSIVE и все остальные.
Если же мы подключились в режиме auto_commit и команда  BEGIN TRANSACTION явно не запускалась, то каждая SQL-команда изменения схемы данных или самих данных будет запускаться отдельной транзакцией без возможности отката (в чем собственно и состоит auto_commit). Такое поведение проще для понимания, но для большого количества запросов не вполне рационально (напр. у меня 30 млн. команд INSERT в таблицу из 8 числовых полей выполнялись 13 минут без auto_commit (в одной транзакции), а с auto_commit дождаться окончания процесса так и не удалось - методом экспликации можно предположить, что процесс длился бы чуть больше месяца).
По умолчанию транзакция запускается в режиме DEFERRED (т.е. отложенный). Его отличие от возможных параметров IMMEDIATE (т.е. непостредственный) и EXCLUSIVE (это слово уже кажется переводить нет нужды) обсуждается ниже на примерах.
Дано: база данных /tmp/test.db с таблицей следующей схемы:
CREATE TABLE t1 (a integer primary key, b text);
Создавать несколько таблиц для нашего тестирования бесполезно т.к. в SQLite 3 транзакция все равно блокирует всю базу (как это ни печально).
Итак, поключаемся к базе двумя CLI клиентами (синий и красный) и начинаем тесты.
Сперва DEFERRED:
sqlite> begin transaction;


sqlite> select * from t1;
sqlite> insert into t1 (b) values ('red insert on deferred');
sqlite> select * from t1;
1|red insert on deferred

Как видно из этого абзаца, хотя транзакция уже запущена, база не заблокирована ни на чтение, ни на запись.

sqlite> insert into t1 (b) values ('blue insert on deffered');

sqlite> select * from t1;
1|red insert on deferred
sqlite> insert into t1 (b) values ('red insert on deferred');
Error: database is locked

а вот после первого INSERT в синем клиенте, красный уже не может писать в базу (такое поведение и называется отложенным). Читать он может, но результатов работы незаконченной "синей" транзакции он не видит - т.к. read_uncommited по умолчанию выключен.

sqlite> commit;

sqlite> select * from t1;
1|red insert on deferred
2|blue insert on deffered
sqlite> insert into t1 (b) values ('red insert on deferred');
sqlite> select * from t1;
1|red insert on deferred
2|blue insert on deffered
3|red insert on deferred

после "синего" COMMIT, "красный" увидел результаты накаченной транзакции - и вновь может писать в базу. Добавим еще, что в нашем случае "красный" работал в режиме auto_commit, но при DEFERRED транзакциях ничего бы не изменилось, если б он запускал BEGIN TRANSACTION, потому что при отложенном поведении база блокируется не началом транзакции, а первым оператором изменения данных в ней (т.е. фактически BEGIN TRANSACTION не делает ничего). 
Теперь посмотрим, как поведут себя те же клиенты при IMMEDIATE транзакциях.
sqlite> begin immediate transaction;

sqlite> select * from t1;
sqlite> insert into t1 (b) values ('red insert on immediate');
Error: database is locked
sqlite> begin immediate transaction;
Error: database is locked

с началом транзакции база заблокирована на запись - вставить запись нельзя, начать транзакцию тоже нельзя.

sqlite> insert into t1 (b) values ('blue insert on immediate');

sqlite> select * from t1;

"красному" ничего не видно - т.к. read_uncommited никто не включал

sqlite> commit;

sqlite> select * from t1;
1|blue insert on immediate
sqlite> insert into t1 (b) values ('red insert on immediate');
sqlite> select * from t1;
1|blue insert on immediate
2|red insert on immediate

после наката транзакции стали видны ее результаты - и снялась блокировка на запись.
Теперь самый суровый вариант транзакции - EXCLUSIVE:
sqlite> begin exclusive transaction;

sqlite> select * from t1;
Error: database is locked
sqlite> insert into t1 (b) values ('red insert on exclusive');
Error: database is locked
sqlite> begin exclusive transaction;
Error: database is locked

"красному" нельзя ничего - база у нас теперь эксклюзивная.

Осталось написать только про read_uncommited. Включается она по идее должна так:
PRAGMA read_uncommited = 1;
Но увидеть ее в деле мне не удалось ни в версии 3.3.6, ни в 3.7.3 (платформа - Linux). Документация безстрастно сообщает, что некоторые прагмы могут не поддерживаться в некоторых версиях - и установка неизвестной прагмы ошибки не вызывает. Вероятно, это тот самый вариант.
Вообще знакомство с транзакциями в SQLite породило у меня ощущение, что ими можно пользоваться лишь для ускорения операций записи в базу данных (см. выше вопиющий пример с 30 млн. записей). Можно ли рассчитывать на них при реальном наличии конкурентного доступа к БД - лично для меня - большой вопрос.

четверг, 30 декабря 2010 г.

Вызов mysql_use_result в Python-MySQL

Точно так же, как и Perl DBI, Python-MySQL по умолчанию использует API функцию mysql_store_result, что на больших объемах выборки (тестировалось с выборкой возвращающей 17 млн. записей) приводит к использованию невероятного количества памяти - и результатов работы скрипта можно не ждать - ибо система будет увлеченно занята копированием страниц из свопа - и в своп. Метод борьбы с этой бедой - тот же, что и в DBI - нужно использовать функцию mysql_use_result (плата за экономию ресурсов проста и жестока - пока result не закрыт, никакой другой запрос на этом подключении выполнить нельзя).
На практике делается это так. При использовании модуля MySQLdb вместо обычного вызова метода cursor() без аргументов пишем:
cu = db.cursor(MySQLdb.cursors.SSCursor)
Скажем несколько слов о поведении данного типа курсоров. Как легко догадаться, привычный вызов fetchall() делает курсор на серверной стороне абсолютно безсмысленным - так что нужно использовать fetchone(). Вот тут-то нас и поджидает небольшая засада - очевидный цикл:
for i in range(0, cu.rowcount):
как раз и не работает, ибо для SSCursor rowcount не определен. Для любителей чисто питонского цикла
for rw in cu:
сразу скажу, что данная конструкция, как ни странно, работает правильно. Так же можно организовать цикл способом аналогичным тому, который предлагается ниже для модуля _mysql.
Так вот, переходим к этому модулю. Выигрыш в производительности он дает выдающийся - на моей выборке быстрее раз в 5 (даже быстрее, чем аналогичный скрипт на Perl), так что ради таких выгод вполне можно потерпеть несколько зубодробительный синтаксис (тот, кто знаком с MySQL C API ничего страшного в этом синтаксисе не увидит).
Работаем оно так:
db.query("select * from sometable")
rs = db.use_result()
Все это еще не беда. Беда начинается дальше. rs.num_rows() по причинам, изложенным выше, не работает, а потому с циклом придется слегка повозиться. Python - это вам не C и не Perl - оператор присваивания внутри условия цикла не поддерживается, и потому нужно организовывать достаточно уродливый цикл (последний раз что-то подобное приходилось делать в языке 1С Бухгалтерии):

rw = rs.fetch_row()
while rw:
    # do something
    rw = rs.fetch_row()
На этом сюрпризы не заканчиваются. rw в этом примере - вовсе не tuple, состоящий из полей запроса, а tuple, состоящий tuples, состоящих из полей (к счастью, по умолчанию, там этот tuple строго один - для возврата нескольких строк за раз fetch_row нужно вызывать с аргументом maxrows - значение 0 означает "все"). Так что присваивание переменным значений полей будет выглядеть так:
fld1, fld2, fld3 = rw[0]
Еще нужно помнить, что если MySQLdb приводит типы данных MySQL к аналогичным типам Python, то в _mysql все данные - тупые строки (если я правильно помню, аналогичные функции C API ведут себя аналогично).

вторник, 23 ноября 2010 г.

Установка debian-testing c USB stick

Оговорюсь с порога: если вы желаете поставить с USB stick debian-stable, то описанная последовательность действий вам вовсе не интересна, ибо готовые LiveUSB образы лежат вот здесь: http://cdimage.debian.org/debian-cd/current-live/i386/usb-hdd/
А вот с testing придется слегка повозиться. Предположим, что имеется установленная система Linux (вероятно, пойдет практически любая работоспособная) и 1 Гб флешка /dev/sdb - дальнейшее является творчески переработанными инструкциями из хорошей статьи  http://d-i.pascal.at/ (язык - англ.)
Из пакетов, которые не всегда входят в дистрибутив, понадобится только syslinux.
Поехали:
# mkfs.msdos -I /dev/sdb
# syslinux /dev/sdb
# mount /dev/sdb /mnt
Берем vmlinuz и initrd.gz напр. отсюда: http://ftp.fi.debian.org/debian/dists/testing/main/installer-i386/current/images/hd-media/ (зеркало debian не обязательно должно быть финским) и копируем их:
# cp vmlinuz initrd.gz /mnt
Берем понравившийся нам iso-образ отсюда: http://cdimage.debian.org/cdimage/weekly-builds/i386/iso-cd/ (мой выбор debian-testing-i386-xfce+lxde-CD-1.iso) и его тоже - туда же (не забудем, что syslinux не понимает длинных имен):
# cp debian-testing-i386-xfce+lxde-CD-1.iso /mnt/netinst.iso
И создаем следующий файл конфигурации загрузчика /mnt/syslinux.cfg:

default vmlinuz
append initrd=initrd.gz ramdisk_size=10240 \
 root=/dev/rd/0 devfs=mount,dall \
 rw DEBCONF_PRIORITY=medium
(последние три строки - в реальности - одна, слешей не надо,  перенесено просто для убористости).
Отмонтируем флешку - грузимся с нее - и наблюдаем привычный установщик debian.

P.S. система почему-то установилась в какой-то достаточно скромной конфигурации (без менеджера рабочего стола), но решение этой проблемы едва ли затруднит средней руки дебианщика. На всякий случай для памяти:
# mount /dev/sdb /mnt
# mount -o loop /mnt/netinst.iso /media/cdrom
# apt-get update
# apt-get install lxde 

среда, 21 июля 2010 г.

Вставка нулевых значений в auto_increment поля в MySQL



По умолчанию, если в поле с опцией auto_increment вставляется значение 0 или null, MySQL заменяет его следующим значением счетчика. Если критично важно вставить именно 0, то нам поможет директива:
sql-mode='NO_AUTO_VALUE_ON_ZERO'
в /etc/my.cnf
или строка:
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
в начале файла дампа.

среда, 14 апреля 2010 г.

Странное поведение mktime относительно летнего времени


На опыте столкнулся с тем, что результат, возвращаемый функцией mktime(3) не учитывает перехода на зимнее/летнее время (т.е. результаты для "лета" оказываются на час больше, чем нужно). Проверено на linux 2.6.18 и FreeBSD 8.0 с корректно установленной зоной MSK/MSD - так что, вероятно, именно такое странное поведение соответствует спецификации POSIX. Самое смешное в том, что (в полном соответствии с документацией) функция mktime не только возвращает типа значение time_t, но и "нормализует" содержимое переданной в нее по указателю struct timespec, заполняя совершенно корректными (учитывающеми перевод времени) значениями поля tm_isdst, tm_gmtoff и tm_zone. Очевидно, где-то в этом направлении и лежит путь борьбы с этой функцией. Т.е вместо очевидного:

struct timespec ts;

time_t tm = mktime(&ts);

Пишем теперь так:

struct timespec ts;

time_t tm = mktime(&ts) - ts.tm_isdst * 3600;

Эстетам же и забывчивым можно предложить определить вот такой макрос:

#define mktime(t) (mktime(t) - (t)->tm_isdst * 3600)

и использовать mktime, как обычно.

Еще идеи?

пятница, 12 марта 2010 г.

Проброс сессии ssh из внешнего мира в немаршрутизируемую сеть (ssh tunnel)



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

Исходные данные

В задаче участвуют три хоста: машина, за клавиатурой которой мы находимся (имя: my-cons, адрес: 11.0.0.11), машина, к которой хотим получить доступ (имя: in-serv, адрес: 10.0.0.10) и машина-шлюз, с которой доступны обе эти машины (имя: gat-serv, адрес: 12.0.0.12). В дальнейшем хост, на котором будет вводиться команда, обозначается приглашением командной строки.
Пусть на всех трех хостах имеется учетная запись bigadm, под которой мы и будем работать. Также предполагается, что на in-serv и gat-serv запущен sshd

О ключах

Т.к. наиболее рациональным способом авторизации в ssh является использование публичных ключей, что напишем, какие ключи, где надо прописать.
  1. ключ my-cons:~bigadm/.ssh/id_rsa.pub должен быть добавлен в in-serv:~bigadm/.ssh/authorized_keys
  2. ключ gat-serv:~bigadm/.ssh/id_rsa.pub должен быть добавлен в gat-serv:~bigadm/.ssh/authorized_keys
  3. т.к. скорее всего процесс настраивается с помощью того же ssh, то ключ my-cons:~bigadm/.ssh/id_rsa.pub не помешает в gat-serv:~bigadm/.ssh/authorized_keys (строго говоря, для решения самой задачи это НЕ необходимо - если у нас напр. при настройке был доступ к консоли этого сервера - или мы производили настройку с какого-то другого хоста).
Сам процесс

Все достаточно просто:
bigadm@gat-serv$ ssh -f -N -g -L 2222:10.0.0.10:22 bigadm@12.0.0.12
(следует обратить внимание на ключ -g - без него 2222 порт открывается на адресе 127.0.0.1 - т.е. туннелем можно воспользоваться только локально - а это вовсе не то, что нам сейчас требуется (хотя применимо напр. для проброса по шифрованному каналу нешифрованного трафика - POP3, etc).
bigadm@my-cons$ ssh -p2222 12.0.0.12
и мы должны оказаться в консоли in-serv.

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

nsswitch.conf для FreeBSD, пересобранной без NIS



Дальнейшие опыты проверены на FreeBSD 8.0

NIS в наше время используется достаточно редко, так что при пересоборке мира существует соблазн добавить в /etc/src.conf строку WITHOUT_NIS=YES (для более ранних версий править надо файл /etc/make.conf). После пересборки с этой опцией все вроде работает, но порой замечаются легкие странности - например, демон cron не отправляет стандартный вывод выполенных комманд по почте, а в /var/log/cron пишет:
NSSWITCH(_nsdispatch): nis, passwd_compat, endpwent, not found, and no fallback provided
Очевидно, проблему надо искать в /etc/nsswitch.conf
Там по умолчанию стоит:
passwd: compat
man-страница nsswitch.conf(5) сообщает, что compat означает подержку в файлах /etc/passwd и /etc/group строк + и - , что на самом деле имеет какой-то смысл только при использовании NIS (кто с ней знаком, помнит, зачем это надо - прочим нормальным людям едва ли необходимо об этом знать). Очевидно, система, собранная без поддержки NIS, просто перестает понимать какой-такой это compat - и пасует. Решение представляется очевидным. Меняем строку на банальное:
passwd: files

...и ожидаем новых сюрпризов. Мораль - хочешь спать спокойно, нечего править /etc/src.conf ;)