Archive for the ‘IT’ Category.

前職在籍中の主要記事へのリンク




Xen 4 で Scientific Linux 6 Dom0

Xen (Xen-PV) は良いです。何が良いって、VT/AMD-V が必須の KVM や Xen-HVM と違って Paravirtualization でも動くので、最近流行りの IaaS のテストなども、VM 上でおこなうことができるところです。(KVM に比べて、サポートが手薄な感は拭えませんが)

RHEL5/CentOS5 では、インストール中に “Virtualization” サポートのパッケージグループを有効にするだけで Xen の Dom0 がインストールできて簡単でした。ところが RHEL6/SL6 (Scientific Linux 6) では、Red Hat による KVM 推しのせいか Xen Dom0 が除外されてしまいました。実運用に使うわけではないので品質は問わないので、SL6 を Xen Dom0 化してみました。

RHEL6 では、Dom0 が除外されただけです。ノーマルカーネルは Xen pv_ops オプション付きでビルドされているので、そのまま Xen DomU にはなれます。

なお、今回は本家の記事がネタ元。参考: “RHEL6Xen4Tutorial – Xen Wiki“。

前準備

まず、distro をインストールする先の VM の設定を変えます。KVM であれば、ストレージやネットワークに virtio を使わず、IDE や SCSI、RTL-8139 や E1000 あたりを利用した方が無難です。別段、Dom0 が virtio を使っちゃいかんこともないと思うのですが、起動・認識しませんでした。カーネルの config 等まで、よくは見ていません。VMware の ESXi でも同様に、SCSI は準仮想化を切り、LSI Logic PATA あたりに設定します (これまた起動しなくなります)。ターゲットとして「RHEL6」等の新しい distro を選択すると virtio を有効にされてしまうこともあるので注意します。

以下の例は、KVM にインストールした SL6 (“Desktop”) です。ありモノのパッケージを使う場合には /boot/ のサイズはそれほど要らないのですが、自分でビルドする場合には、巨大な全部入り initramfs を収めることになりますので、/boot/ に 1~2GB ほど割り当てておきます。それと、”Desktop” インストールをした後でビルドを行なうと、ざっと 12~13GB にはなります。

以下、ほめられたことではありませんが、面倒なのですべて root で作業します。SELinux と iptables も off にします。

[root@sl6x ~]# chkconfig iptables off
[root@sl6x ~]# cp /etc/sysconfig/selinux /etc/sysconfig/selinux.orig
[root@sl6x ~]# vi /etc/sysconfig/selinux
[root@sl6x ~]# diff -uNr /etc/sysconfig/selinux.orig /etc/sysconfig/selinux
--- /etc/sysconfig/selinux.orig
+++ /etc/sysconfig/selinux
@@ -4,7 +4,7 @@
 #     enforcing - SELinux security policy is enforced.
 #     permissive - SELinux prints warnings instead of enforcing.
 #     disabled - No SELinux policy is loaded.
-SELINUX=enforcing
+SELINUX=disabled
 # SELINUXTYPE= can take one of these two values:
 #     targeted - Targeted processes are protected,
 #     mls - Multi Level Security protection.

下記の組み合わせ (特に「ネット上で配布されているバイナリを利用する」場合) で、libvirt の virbr0 (NAT) 経由で Fedora 14 や SL6 を入れようとすると、どうも pv_ops な DomU のネットワークが不安定なのか、不定なタイミングでインストールがコケます。カーネルだけは手で入れた方が無難かも知れません。あるいは、下記のようにして直結ブリッジを作っておきます。(xenbr0 や peth0 が見当たりません。Xen 4 になって、見せないようになった?)

なお、”NM_CONTROLLED” の行を “no” にするやいなや NetworkManager はインターフェイスを off にしますので、リモートから編集していると切られます。

[root@sl6x ~]# cd /etc/sysconfig/network-scripts/
[root@sl6x network-scripts]# cp ifcfg-eth0 /tmp/ifcfg-eth0
[root@sl6x network-scripts]# cp ifcfg-eth0 ifcfg-br0
[root@sl6x network-scripts]# vi ifcfg-eth0
[root@sl6x network-scripts]# vi ifcfg-br0
[root@sl6x network-scripts]# diff -uNr /tmp/ifcfg-eth0 ifcfg-eth0
--- /tmp/ifcfg-eth0
+++ ifcfg-eth0
@@ -1,5 +1,4 @@
 DEVICE="eth0"
-BOOTPROTO="dhcp"
-HWADDR="XX:XX:XX:XX:XX:XX"
-NM_CONTROLLED="yes"
+NM_CONTROLLED="no"
 ONBOOT="yes"
+BRIDGE="br0"
[root@sl6x network-scripts]# diff -uNr /tmp/ifcfg-eth0 ifcfg-br0
--- /tmp/ifcfg-eth0
+++ ifcfg-br0
@@ -1,5 +1,5 @@
-DEVICE="eth0"
+DEVICE="br0"
 BOOTPROTO="dhcp"
-HWADDR="XX:XX:XX:XX:XX:XX"
-NM_CONTROLLED="yes"
+NM_CONTROLLED="no"
 ONBOOT="yes"
+TYPE="Bridge"
[root@sl6x network-scripts]# cd
[root@sl6x ~]# service network restart

計算ノードとして VM イメージをコピーする際には、当然 NIC の MAC アドレスも変わります。そのため、MAC アドレスを記憶されてしまうと不都合ですので、消して、二度と書かれないようにしておきます。参考: “Disable automatic udev rules for network interfaces in Ubuntu << six degrees of freedom“。

[root@sl6x ~]# rm -f /etc/udev/rules.d/70-persistent-net.rules
[root@sl6x ~]# mkdir /etc/udev/rules.d/70-persistent-net.rules
[root@sl6x ~]#

SELinux もオフにしたことですし、ここらで一度リブートしておきますか。

[root@sl6x ~]# reboot

ネット上で配布されているバイナリを利用する場合

コミュニティビルドのパッケージです。ドイツ語は読めませんが、ありがたく利用させていただきます。

[root@sl6x ~]# ( cd /etc/yum.repos.d/ ; wget \
 http://www.gitco.de/linux/x86_64/centos/6/gitco-centos6-x86_64.repo )
(中略)
2011-05-21 15:17:02 (62.2 MB/s) - “gitco-centos6-x86_64.repo” saved [607/607]
[root@sl6x ~]# yum install -y kernel # ← Dom0 カーネルが入らなければ要調整
(中略)
Installed:
  kernel.x86_64 0:2.6.32.26-174.1.xendom0.el6

Dependency Updated:
  kernel-firmware.x86_64 0:2.6.32.26-174.1.xendom0.el6

Complete!
[root@sl6x ~]# yum install -y \
 xen xen-hypervisor xen-libs xen-licenses xen-runtime
(中略)
Installed:
  xen.x86_64 0:4.0.1-6.1.el6            xen-hypervisor.x86_64 0:4.0.1-6.1.el6
  xen-libs.x86_64 0:4.0.1-6.1.el6       xen-licenses.x86_64 0:4.0.1-6.1.el6
  xen-runtime.x86_64 0:4.0.1-6.1.el6

Dependency Installed:
  PyXML.x86_64 0:0.8.4-19.el6              SDL.x86_64 0:1.2.14-2.el6
  qemu-common.x86_64 2:0.12.5-1.el6        qemu-img.x86_64 2:0.12.5-1.el6

Complete!
[root@sl6x ~]#

上記の手順で、カーネルよりも後で xen-runtime を入れることで、postinstall のスクリプトで grub.conf を適切に編集してくれます。でもこれ、本来は trigger スクリプトでやるべきなのでは? もし xen.gz のエントリが書かれていないようであれば、以下のように手で書き換えます。

[root@sl6x ~]# cp /boot/grub/grub.conf /boot/grub/grub.conf.orig
[root@sl6x ~]# vi /boot/grub/grub.conf
[root@sl6x ~]# diff -uNr /boot/grub/grub.conf.orig /boot/grub/grub.conf
--- /boot/grub/grub.conf.orig
+++ /boot/grub/grub.conf
@@ -13,8 +13,9 @@
 hiddenmenu
 title Scientific Linux (2.6.32.26-174.1.xendom0.el6.x86_64)
        root (hd0,0)
-       kernel /vmlinuz-2.6.32.26-174.1.xendom0.el6.x86_64 ro (中略)
-       initrd /initramfs-2.6.32.26-174.1.xendom0.el6.x86_64.img
+       kernel=/xen.gz
+       module /vmlinuz-2.6.32.26-174.1.xendom0.el6.x86_64 ro (中略)
+       module /initramfs-2.6.32.26-174.1.xendom0.el6.x86_64.img
 title Scientific Linux (2.6.32-71.24.1.el6.x86_64)
        root (hd0,0)
        kernel /vmlinuz-2.6.32-71.24.1.el6.x86_64 ro (中略)
[root@sl6x ~]#

Xen で KSM (Kernel Samepage Merging) は未サポートらしいので ksm と ksmtuned も切っておきます。

[root@sl6x ~]# chkconfig ksm off
[root@sl6x ~]# chkconfig ksmtuned off
[root@sl6x ~]#

Xen を、libvirt 経由で使います。UI としては、virt-manager が良いでしょう。python-virtinst を使いたかったのですが、Xen 4 に追随していないのか、blktap2 まわりで正しく動きませんでした。

[root@sl6x ~]# yum install -y virt-manager libvirt
(中略)
Installed:
  libvirt.x86_64 0:0.8.1-27.1.el6       virt-manager.noarch 0:0.8.4-8.el6

Dependency Installed:
  augeas-libs.x86_64 0:0.7.2-3.el6
  ebtables.x86_64 0:2.0.9-5.el6
  gtk-vnc.x86_64 0:0.3.10-3.el6
  gtk-vnc-python.x86_64 0:0.3.10-3.el6
  iscsi-initiator-utils.x86_64 0:6.2.0.872-10.el6
  libvirt-client.x86_64 0:0.8.1-27.1.el6
  libvirt-python.x86_64 0:0.8.1-27.1.el6
  lzo.x86_64 0:2.03-3.1.el6
  lzop.x86_64 0:1.02-0.9.rc1.el6
  nc.x86_64 0:1.84-22.el6
  netcf-libs.x86_64 0:0.1.6-4.el6
  numactl.x86_64 0:2.0.3-9.el6
  python-virtinst.noarch 0:0.500.3-7.el6
  yajl.x86_64 0:1.0.7-3.el6

Complete!
[root@sl6x ~]#

以上です。再起動して、Xen + Dom0 でブートします。

[root@sl6x ~]# reboot

再起動したら、とりあえず Dom0 で動いていることを確認してみます。

[root@sl6x ~]# virsh list
 Id Name                 State
----------------------------------
  0 Domain-0             running

[root@sl6x ~]#

テストとして、Virt-Manager から Scientific Linux 6 を ttp://ftp.riken.jp/Linux/scientific/6.0/x86_64/os/ からネットワークインストールしてみます。良いようです。ttp://rsync.atworks.co.jp/centos/5/os/i386/ も行けました。

ところで、UltraVNC Win32 Viewer で QEMU-KVM に繋いだ中で Virt-Manager の VNC クライアントが上がると、とても見づらい画面になるのは私だけ? VNC クライアントのせいかしら。

自前でビルドする場合

まずは、標準レポジトリのパッケージをインストールします。

[root@sl6xdev ~]# yum groupinstall -y \
 "Development tools" \
 "Additional Development" "Debugging Tools" \
 "System administration tools" \
 "Compatibility libraries" "Console internet tools" \
 "Desktop Platform Development"
(中略)
Complete!
[root@sl6xdev ~]# yum install -y \
 transfig wget texi2html libaio-devel dev86 \
 glibc-devel e2fsprogs-devel gitk mkinitrd iasl xz-devel \
 bzip2-devel pciutils-libs pciutils-devel SDL-devel \
 libX11-devel gtk2-devel bridge-utils PyXML qemu-common \
 qemu-img mercurial texinfo libuuid-devel
(中略)
Complete!
[root@sl6xdev ~]# yum install -y glibc-devel.i686
(中略)
Complete!
[root@sl6xdev ~]#

Xen 4 のビルドとインストールをします。

[root@sl6xdev ~]# host=ftp-srv2.kddilabs.jp
[root@sl6xdev ~]# path=/Linux/packages/fedora/releases/14/Everything/source/SRPMS
[root@sl6xdev ~]# ver=4.0.1-6.fc14
[root@sl6xdev ~]# wget http://$host/$path/xen-$ver.src.rpm
(中略)
[root@sl6xdev ~]# rpmbuild --rebuild xen-$ver.src.rpm
(中略)
Wrote: /root/rpmbuild/RPMS/x86_64/xen-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-libs-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-runtime-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-hypervisor-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-doc-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-devel-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-licenses-4.0.1-6.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/xen-debuginfo-4.0.1-6.el6.x86_64.rpm
(中略)
+ exit 0
[root@sl6xdev ~]# path=Linux/packages/fedora/releases/13/Fedora/source/SRPMS
[root@sl6xdev ~]# ver=0.12.3-8.fc13
[root@sl6xdev ~]# http://$host/$path/qemu-$ver.src.rpm
(中略)
[root@sl6xdev ~]# rpmbuild --rebuild qemu-$ver.src.rpm
(中略)
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-kvm-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-img-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-common-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-user-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-x86-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-ppc-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-sparc-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-arm-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-mips-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-cris-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-m68k-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-system-sh4-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-kvm-tools-0.12.3-8.el6.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/qemu-debuginfo-0.12.3-8.el6.x86_64.rpm
(中略)
+ exit 0
[root@sl6xdev ~]# rpm -Uvh \
 ~/rpmbuild/RPMS/x86_64/xen-runtime-4* \
 ~/rpmbuild/RPMS/x86_64/xen-libs-4* \
 ~/rpmbuild/RPMS/x86_64/xen-licenses-4* \
 ~/rpmbuild/RPMS/x86_64/xen-4* \
 ~/rpmbuild/RPMS/x86_64/xen-hypervisor-4* \
 ~/rpmbuild/RPMS/x86_64/qemu-common-*
(中略)
[root@sl6xdev ~]# chkconfig ksm off
[root@sl6xdev ~]# chkconfig ksmtuned off
[root@sl6xdev ~]#

Xen Dom0 カーネルのビルドとインストールをします。

[root@sl6xdev ~]# git clone \
 git://git.kernel.org/pub/scm/linux/kernel/git/jeremy/xen.git linux-2.6-xen
Initialized empty Git repository in /root/linux-2.6-xen/.git/
(中略)
[root@sl6xdev ~]# cd linux-2.6-xen
[root@sl6xdev linux-2.6-xen]# host=pasik.reaktio.net
[root@sl6xdev linux-2.6-xen]# wget -O .config \

http://$host/xen/kernel-config/config-2.6.32.25-pvops-dom0-xen-stable-x86_64

(中略)
[root@sl6xdev linux-2.6-xen]# make oldconfig
(質問が出たら答えます)
[root@sl6xdev linux-2.6-xen]# jobs=$(grep ^processor /proc/cpuinfo | wc -l)
[root@sl6xdev linux-2.6-xen]# make -j$jobs bzImage && \
 make -j$jobs modules && make modules_install
(中略)
  INSTALL /lib/firmware/cpia2/stv0672_vp4.bin
  INSTALL /lib/firmware/yam/1200.bin
  INSTALL /lib/firmware/yam/9600.bin
  DEPMOD  2.6.32.40
[root@sl6xdev linux-2.6-xen]# ver=$(make kernelversion)
[root@sl6xdev linux-2.6-xen]# depmod -a $ver
[root@sl6xdev linux-2.6-xen]# cp -a arch/x86/boot/bzImage /boot/vmlinuz-$ver
[root@sl6xdev linux-2.6-xen]# cp -a System.map /boot/System.map-$ver
[root@sl6xdev linux-2.6-xen]# cp -a .config /boot/config-$ver
[root@sl6xdev linux-2.6-xen]# cd /boot/
[root@sl6xdev boot]# dracut initramfs-$ver.img $ver
[root@sl6xdev boot]# cp /boot/grub/grub.conf /boot/grub/grub.conf.orig
[root@sl6xdev boot]# vi /boot/grub/grub.conf
[root@sl6xdev boot]# diff -uNr /boot/grub/grub.conf.orig /boot/grub/grub.conf
--- /boot/grub/grub.conf.orig
+++ /boot/grub/grub.conf
@@ -11,6 +11,11 @@
 timeout=5
 splashimage=(hd0,0)/grub/splash.xpm.gz
 hiddenmenu
+title Scientific Linux Dom0
+        root (hd0,0)
+        kernel /xen.gz dom0_mem=1024M loglvl=all guest_loglvl=all
+        module /vmlinuz-2.6.32.40 ro root=(中略)
+        module /initramfs-2.6.32.40.img
 title Scientific Linux (2.6.32-71.24.1.el6.x86_64)
        root (hd0,0)
        kernel /vmlinuz-2.6.32-71.24.1.el6.x86_64 ro root=(中略)
[root@sl6xdev boot]# cd

標準の libvirt では Xen API サポートが落ちているので、有効にしてビルドします。Dom0 カーネル上でビルドするとチェックでコケるらしいので、一応テストをオフにしておきます。virt-manager も入れておきます。

[root@sl6xdev ~]# yumdownloader --source libvirt
(中略)
libvirt-0.8.1-27.el6_0.5.src.rpm                         |  11 MB     00:06
[root@sl6xdev ~]# rpm -i libvirt-*.src.rpm
[root@sl6xdev ~]# cp rpmbuild/SPECS/libvirt.spec \
 rpmbuild/SPECS/libvirt.spec.orig
[root@sl6xdev ~]# vi rpmbuild/SPECS/libvirt.spec
[root@sl6xdev ~]# diff -uNr rpmbuild/SPECS/libvirt.spec.orig \
 rpmbuild/SPECS/libvirt.spec
--- rpmbuild/SPECS/libvirt.spec.orig
+++ rpmbuild/SPECS/libvirt.spec
@@ -107,7 +107,7 @@
 %ifnarch x86_64
 %define with_qemu 0
 %endif
-%define with_xen 0
+%define with_xen 1
 %endif

 # If Xen isn't turned on, we shouldn't build the xen proxy either
@@ -1362,20 +1362,6 @@
 %clean
 rm -fr %{buildroot}

-%check
-cd tests
-# These 3 tests don't current work in a mock build root
-for i in nodeinfotest daemon-conf seclabeltest
-do
-  rm -f $i
-  echo -e "#!/bin/sh\nexit 0" > $i
-  chmod +x $i
-done
-# The test applied by patch need to be made executable
-chmod +x virsh-schedinfo
-
-make check
-
 %pre
 %if 0%{?fedora} >= 12 || 0%{?rhel} >= 6
 # Normally 'setup' adds this in /etc/passwd, but this is
[root@sl6xdev ~]# yum-builddep -y libvirt
(中略)
Complete!
[root@sl6xdev ~]# rpm -Uvh rpmbuild/RPMS/x86_64/xen-devel-*.rpm
[root@sl6xdev ~]# yum install -y libpcap-devel
(中略)
Complete!
[root@sl6xdev ~]# rpmbuild -ba rpmbuild/SPECS/libvirt.spec
(中略)
Wrote: /root/rpmbuild/SRPMS/libvirt-0.8.1-27.el6.5.src.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/libvirt-0.8.1-27.el6.5.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/libvirt-client-0.8.1-27.el6.5.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/libvirt-devel-0.8.1-27.el6.5.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/libvirt-python-0.8.1-27.el6.5.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/libvirt-debuginfo-0.8.1-27.el6.5.x86_64.rpm
Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.aEw602
+ umask 022
+ cd /root/rpmbuild/BUILD
+ cd libvirt-0.8.1
+ rm -fr /root/rpmbuild/BUILDROOT/libvirt-0.8.1-27.el6.5.x86_64
+ exit 0
[root@sl6xdev ~]# yum install -y nc ebtables lzop
(中略)
Complete!
[root@sl6xdev ~]# rpm -Uvh \
 rpmbuild/RPMS/x86_64/libvirt-client-0.8.1-27.el6.5.x86_64.rpm \
 rpmbuild/RPMS/x86_64/libvirt-0.8.1-27.el6.5.x86_64.rpm \
 rpmbuild/RPMS/x86_64/libvirt-python-0.8.1-27.el6.5.x86_64.rpm
[root@sl6xdev ~]# yum install -y virt-manager
(中略)
Complete!
[root@sl6xdev ~]#

リブートします。

[root@sl6xdev ~]# reboot

先の例と同様に、Dom0 で動作していることを確認し、適当にインストールをしてみます。良いようです。




Scala: Haskell 本の引き写しでパーサジェネレータ

プログラミング Haskell」の第 8 章「関数型パーサ」の写経をしようかと思ったのですが、そのまま引き写しても面白くないので、Scala で書いてみました。せっかくですので、Haskell のモナド連結の糖衣構文 do を、極力そのまま Scala の for 式で書けるよう、Parser[A] には、map(), flatMap() を備え、Scala の型とします。

…できた。Haskell 版から変えたところとしては、入力文字列にパーサを適用した際には、Scala らしく Option[(A, String)] を返すようにした、Haskell で [Char] であるところは、適宜 String にした、程度か。案外そのまま行けますね、というよりも、Scala が Haskell を色濃く反映しているんですね。

abstract trait Parser[A] {
  def apply(inp: String): Option[(A, String)]
  def parse(inp: String) = apply(inp)
  def map[B](f: (A) => B) = {
    def p_parse = parse _
    new Parser[B] {
      def apply(inp: String) = {
        p_parse(inp) match {
          case None => None
          case Some((v, out)) => Some((f(v), out))
        }
      }
    }
  }
  def flatMap[B](f: (A) => Parser[B]): Parser[B] = {
    def p_parse = parse _
    new Parser[B] {
      def apply(inp: String) = {
        p_parse(inp) match {
          case None => None
          case Some((v, out)) => f(v)(out)
        }
      }
    }
  }
  def +++(q: Parser[A]) = {
    def p_parse = parse _
    new Parser[A] {
      def apply(inp: String) = {
        p_parse(inp) match {
          case None => q.parse(inp)
          case s @ Some(_) => s
        }
      }
    }
  }
}

def return_[A](v: A): Parser[A] = {
  new Parser[A] { def apply(inp: String) = { Some((v, inp)) } }
}

def failure[A]: Parser[A] = {
  new Parser[A] { def apply(inp: String) = { None } }
}

def item: Parser[Char] = new Parser[Char] {
  def apply(inp: String) = inp match {
    case "" => None
    case _ => Some((inp.head, inp.tail))
  }
}

def parse[A](p: Parser[A], inp: String): Option[(A, String)] = p(inp)

/*

parse(return_(1), "abc")
res1: Option[(Int, String)] = Some((1,abc))

parse(failure, "abc")
res2: Option[(Nothing, String)] = None

parse(item, "abc")
res3: Option[(Char, String)] = Some((a,bc))

 */

/*

def p = for {
  x <- item
  _ <- item
  y <- item
} yield (x, y)
p: Parser[(Char, Char)]

def p_dash = item.flatMap(x =>
  item.flatMap(__ =>
    item.map(y =>
      (x, y) )))
p_dash: Parser[(Char, Char)]

parse(p, "abcdef")
res4: Option[((Char, Char), String)] = Some(((a,c),def))

parse(p, "ab")
res5: Option[((Char, Char), String)] = None

 */

/*

parse(item +++ return_('d'), "abc")
res6: Option[(Char, String)] = Some((a,bc))

parse(failure +++ return_('d'), "abc")
res7: Option[(Char, String)] = Some((d,abc))

parse(failure +++ failure, "abc")
res8: Option[(Nothing, String)] = None

 */

def sat(p: (Char) => Boolean): Parser[Char] = for {
  x <- item
  r <- if (p(x)) return_(x) else failure
} yield (r)

def digit = sat(_.isDigit)

def lower = sat(_.isLower)

def upper = sat(_.isUpper)

def letter = sat(_.isLetter)

def alphanum = letter +++ digit

def char(x: Char) = sat(x.==)

/*

parse(digit, "123")
res9: Option[(Char, String)] = Some((1,23))

parse(digit, "abc")
res10: Option[(Char, String)] = None

parse(char('a'), "abc")
res11: Option[(Char, String)] = Some((a,bc))

parse(char('a'), "123")
res12: Option[(Char, String)] = None

 */

// これは文字列を返すパーサとする。Scala の String は List[Char] ではない
def string(s: String): Parser[String] = s match {
  case "" => return_("")
  case _ => for {
    x <- char(s.head)
    xs <- string(s.tail)
  } yield (x +: xs)
}

/*

parse(string("abc"), "abcdef")
res13: Option[(String, String)] = Some((abc,def))

parse(string("abc"), "ab1234")
res14: Option[(String, String)] = None

 */

object Many {
  def many[A](p: Parser[A]): Parser[List[A]] = {
    many1(p) +++ return_(Nil)
  }
  def many1[A](p: Parser[A]): Parser[List[A]] = {
    for {
      v <- p
      vs <- many(p)
    } yield (v :: vs)
  }
}
import Many._

/*

parse(many(digit), "123abc")
res15: Option[(List[Char], String)] = Some((List(1, 2, 3),abc))

parse(many(digit), "abcdef")
res16: Option[(List[Char], String)] = Some((List(),abcdef))

parse(many1(digit), "abcdef")
res17: Option[(List[Char], String)] = None

 */

// 仕方ないので文字列にしておく
def ident = for {
  x <- lower
  xs <- many(alphanum)
} yield ((x :: xs).foldLeft("")(_ + _))

// これも
def nat: Parser[Int] = for {
  xs <- many1(digit)
} yield (xs.foldLeft("")(_ + _).toInt)

def space: Parser[Unit] = for {
  _ <- many(sat(_.isWhitespace))
} yield (Unit)

/*

parse(ident, "abc def")
res0: Option[(java.lang.String, String)] = Some((abc, def))

parse(nat, "123 abc")
res1: Option[(Int, String)] = Some((123, abc))

parse(space, " abc")
res2: Option[(Unit, String)] = Some(((),abc))

 */

def token[A](p: Parser[A]): Parser[A] = for {
  _ <- space
  v <- p
  _ <- space
} yield (v)

def identifier: Parser[String] = token(ident)

def natural: Parser[Int] = token(nat)

def symbol(xs: String): Parser[String] = token(string(xs))

/*

def p: Parser[List[Int]] = for {
  _ <- symbol("[")
  n <- natural
  ns <- many(
    for {
      _ <- symbol(",")
      x <- natural
    } yield (x)
  )
  _ <- symbol("]")
} yield (n :: ns)

parse(p, " [1, 2, 3] ")
res19: Option[(List[Int], String)] = Some((List(1, 2, 3),))

parse(p, "[1, 2,]")
res20: Option[(List[Int], String)] = None

 */

def int: Parser[Int] = (
  for {
    _ <- symbol("-")
    n <- natural
  } yield (- n)
) +++ natural

object Expr {
  def expr: Parser[Int] = for {
    t <- term
    r <- (
      for {
        _ <- symbol("+")
        e <- expr
      } yield (t + e)
    ) +++ (
      for {
        _ <- symbol("-")
        e <- expr
      } yield (t - e)
    ) +++ return_(t)
  } yield (r)
  def term: Parser[Int] = for {
    f <- factor
    r <- (
      for {
        _ <- symbol("*")
        t <- term
      } yield (f * t)
    ) +++ (
      for {
        _ <- symbol("/")
        t <- term
      } yield (f / t)
    ) +++ return_(f)
  } yield (r)
  def factor: Parser[Int] = (
    for {
      _ <- symbol("(")
      e <- expr
      _ <- symbol(")")
    } yield (e)
  ) +++ int
}
import Expr._

/*

parse(expr, "2*3+4")
res26: Option[(Int, String)] = Some((10,))

parse(expr, "2*(3+4)")
res28: Option[(Int, String)] = Some((14,))

parse(expr, "2 * (3 + 4)")
res30: Option[(Int, String)] = Some((14,))

parse(expr, "2*3-4")
res33: Option[(Int, String)] = Some((2,))

parse(expr, "-1")
res36: Option[(Int, String)] = Some((-1,))

parse(expr, "(-3 * 4 * - 2) / 6")
res43: Option[(Int, String)] = Some((4,))

 */



Scala: エラトステネスのふるいで無限長素数列

簡潔すぎワロタw

import Stream._
def sieve[A <% BigInt](xxs: Stream[A]): Stream[A] = xxs match {
  case p #:: xs => cons(p, sieve(xs.filter(_ % p != 0)))
}
lazy val primes = sieve(from(2))

先頭 10 個。

scala> primes.take(10).toList
res0: List[Int] = List(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)

Fri Apr 22 2011: ついでに、遅延評価版のクイックソートも。まずは、普通に eager 評価のリストで書いた場合:

def qsort[A <% Ordered[A]](xxs: List[A]): List[A] = xxs match {
  case Nil => Nil
  case p :: xs =>
    qsort(xs filter(_ <= p)) ++ (p :: qsort(xs filter(p < _)))
}

ストリームで書くと、以下のように:

import Stream._
def qsort[A <% Ordered[A]](xxs: Stream[A]): Stream[A] = xxs match {
  case Empty => Empty
  case p #:: xs =>
    qsort(xs filter(_ < p)) ++ (p #:: qsort(xs filter(p <= _)))
}

ほぼ同じですが、”Empty” や “#::” などが特徴的です。不用意に無限長ストリームのソートなんかしちゃダメですよ。filter() が返ってきません。




WSH JScript の REPL を書いてみた

秀丸エディタ用、Emacs の eval-print-last-sexp 風マクロ for Scala, Clojure, Gauche, Groovy, Python and Ruby – Ayutaya.com」で、REPL が無いので流していた WSH の JScript (JavaScript) ですが、eval() があるのだからサクッと書けば良いので、どうせ世の中にはすでにたくさんの実装があるとは思われますが、書いてみました。お入り用でしたらこちらからどうぞ。

eval() が環境を汚すので、識別子には “_” を前置しています。それでも衝突すると言うならば、”_” を “_HOGEHOGE_” とでも全置換しても平気なように書かれています。ダブルクォートもバックスラッシュも使っていないので、別プログラム内へ埋め込むのも簡単です。一点ハマったのは、eval() は現在の環境内で実行されるので、当初別ファイルをロードする処理を関数にしたら、呼ばれた関数内で eval() されてしまい、そこから戻ったら “var” も “function” も消えていたことです。仕方がないので、入力ハンドラをスタックするようにしています。

実行例としては、以下のような感じです。

C:\>cscript.exe wshrepl.js
Microsoft (R) Windows Script Host Version 5.7
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

> 1 + 2 +
|   3 + 4
= 10
> function foo() {
|   WScript.Echo("Hello!");
| }
= undefined
> foo()
Hello!
= undefined
> :load c:\priv\prog\bat\wsh.js
= Hello, World!
= undefined
> ^Z

C:\>

以下は、ついでの tips です。

最新のブラウザの JavaScript に比べるといろいろと見劣りする Jscript ではありますが、そこは腐っても JavaScript 1.5 ですから、prototype を拡張して、map(), reduce(), filter() その他を拡張しておくと、なかなかに使える環境になります。

include が無い JScript ですが、そこは eval() で何とでもなります。以下は、実行スクリプトと同一ディレクトリにある “wsh.js” を初期化ファイルとして取り込みます。記述を短くしようと思って “WScript” を “with” すると、なぜか挙動がおかしくなります。

eval(WScript.CreateObject('Scripting.FileSystemObject').OpenTextFile(
 WScript.ScriptFullName.slice(0,-WScript.ScriptName.length)+'wsh.js' ).ReadAll());



秀丸エディタ用、Emacs の eval-print-last-sexp 風マクロ for Scala, Clojure, Gauche, Groovy, Python, Ruby, JScript, Haskell and Perl

Emacs の lisp-interaction-mode に、編集中のテキスト上のカーソル直前位置の Emacs Lisp の S 式を評価する便利なコマンド “eval-print-last-sexp” がありますが、その秀丸エディタ用、各種プログラミング言語対応版です。下記は HTML で文章を書きながら、その中の Clojure のサンプルコードを評価している例です。矢印位置にカーソル (|) がある状態で、この場合は “eval-clojure.mac ” を実行してみます。

<p>下記は、クイックソートのサンプルです:

<pre>
(defn qsort [[x & xs]]
  (when x
    (let [smaller #(< % x)]
      (lazy-cat
        (qsort (filter smaller xs))
        [x]
        (qsort (remove smaller xs)) ))))| ←
</pre>

定義が評価されてシンボルが返ります。

<p>下記は、クイックソートのサンプルです:

<pre>
(defn qsort [[x & xs]]
  (when x
    (let [smaller #(< % x)]
      (lazy-cat
        (qsort (filter smaller xs))
        [x]
        (qsort (remove smaller xs)) ))))
#'user/qsort ←
</pre>

そのまま試しに実行してみます。

(qsort '(5 3 8 9 2))|

その場に結果が返ります。

(qsort '(5 3 8 9 2))
(2 3 5 8 9)

いずれも、日本語も通ります。下記は Scala の例です。

val l = "日本語" :: "テキスト" :: Nil
l: List[java.lang.String] = List(日本語, テキスト)

どこか、昔の Basic インタプリタで開発をしていたような快適さがあります (あいにく SmallTalk の経験は無い)。とりわけ、関数型言語と相性が良いんでないかしら、このスタイルは。メリットとしては、以下のような感じかと思います:

  • 言語をその場でチャンポンに書ける: ガッツリとアプリを開発するのであれば Emacs なり Eclipse なりの各言語のモードを利用すれば良いのでしょうが、つらつらと考えながら各言語を横断的に、サンプルなどのスクリプト片を次々に試しながらコメントも書きたい場合などには、同一テキスト上でその場で評価し、すぐに結果が返る方が快適です
  • コピペをしなくてすむ: いちいちエディタ上で書いて、それを REPL にコピペ実行、というのは、思考を阻害します
  • 起動オーバーヘッドが無い: REPL のセッションをバックグラウンドで維持しますので、とりわけ Scala や Clojure などの、JVM を使う、起動が異様に遅い REPL でも、2 度目以降の評価はサクサクです。非力なノートマシンには、これがありがたい
  • エディタから離れなくて済む: 一日のほとんどをエディタ上で過ごす身としては、ひととおりのスクリプト実行がその場でできるというのはありがたいです。コード片も、無くさず記録として残りますし
  • キー一発: キーを割り当てておけば、普段書きのテキストから各言語にキー一発 (2 ストロークならば二発ですが) でアクセスできます

秀丸エディタは、ver .8.0 以降のマクロで COM オブジェクトの操作をサポートしたため、以前にはできなかった複雑なアプリケーション連携や、テキストを開いている間だけ永続するオブジェクト等を持つことができるようになりました。今回のマクロも、バックグラウンドで各言語の REPL セッションを起動して、テキストをクローズするまでの間、状態を維持することができるようになったことで実現できました。

詳細は以下ですが、詳しくはコードを読んでください。なお、範囲選択をして実行すれば、選択範囲を評価します。

マクロ名 カーソル以前のどこまでを一単位として評価? 入出力方式 既知の問題
eval-scala.mac カーソルのある行、あるいは対応する括弧かヒアドキュメントによって継続している複数行 (Scala に “\” による行継続は無い) 入力は REPL の :load、出力は STDOUT (SJIS) あいかわらず初回起動が遅いが、こればかりはどうしようもない
eval-clojure.mac カーソル直前の S 式 入力は専用 REPL、出力は STDOUT (SJIS) 初回起動は、そこそこに遅い。STDERR をファイルに出力する方法が分からなかったが、多分あまり支障はない
eval-gauche.mac カーソル直前の S 式 入出力とも専用 REPL (UTF-8) 特に無い。Scheme は Gauche がお気に入りなので、Guile は確認していない。
eval-groovy.mac カーソルのある行、あるいは対応する括弧かヒアドキュメントによって継続している複数行、あるいは “\” で継続している複数行 入力は REPL の \l、出力は STDOUT (SJIS) jline が “unix” モードでないと STDIN を正しく扱えなかったため、動作に MinGW の sh(1) と stty(1) が必要
eval-python.mac 第一カラムが空白ではないカーソル行、あるいはカーソル行以前で第一カラムが空白ではない行までの複数行、あるいは “\” で行継続をしている複数行 入力は専用の REPL、出力は STDOUT (UTF-8) REPL の返事が淡白すぎて、返り値が分かりづらい
eval-ruby.mac カーソルのある行、あるいはカーソル行上の “end” とカラムが合う直前の開始行までの複数行、あるいは “\” で行継続している複数行 入力は REPL の irb_source()、出力は STDOUT (SJIS) irb_source() が、たまにファイルハンドルを離してくれないのか、一時ファイルを delete できないことがある。上書きするので、おそらく動作に支障はない
eval-jscript.mac Groovy とだいたい同じ (本当なら、いろいろ違うんだけど…) 入力は自前実装の REPL で “:load”、出力は STDOUT (SJIS) :load は、通常の eval() ではなく、REPL 的に行単位で評価されるので注意。ブロック内の空行はインデントしておいてください
eval-haskell.mac 第 1 カラム目が空白であれば、直前の “module” までをモジュールとしてロードする。第 1 カラム目が空白でなければ、式として解釈する モジュールの入力は REPL の “:load”、式の入力は STDIN から、出力は STDOUT GHC では、日本語入力は Unicode に落ちるものの、出力はできないようだ
eval-perl.mac カーソル行か、括弧を辿って戻れるところまで 入力は自前 REPL の “:load” から、出力は STDOUT から Perl の eval() は新しくブロックを作ってしまうので、評価単位内の “my”, “local” 変数や “$1″ などは、eval() が終わると破棄されてしまう。”$s =~ /([a-z]+)/; $mstr = $1″ のように複文で保持するなど要工夫

あとは Haskell (トップレベルの扱いがよく分からなくて作れなかった) と、JavaScript (WSH には REPL が無い) 用が欲しいところです。

Wed Apr 13 2011: JScript 用を追加。REPL 単体については「WSH JScript の REPL を書いてみた – Ayutaya.com」から。

Fri Apr 15 2011: Haskell (GHC) 用を追加。言語が言語なので、少々特殊。

このカーソル位置(↓)でマクロを起動すると、

module Test where
  fib 1 = 1
  fib 2 = 1
  fib n = fib (n - 1) + fib (n - 2)
|

モジュールをロード。もう一度実行すれば、再ロード。

module Test where
  fib 1 = 1
  fib 2 = 1
  fib n = fib (n - 1) + fib (n - 2)

[1 of 1] Compiling Test
( C:\DOCUME~1\KIICHI~1\LOCALS~1\Temp\haskell_input_2821200.hs, interpreted )
Ok, modules loaded: Test.
|

続けて式を評価すると、

fib 10

値が返る。

fib 10
55|

Sun Apr 17 2011: Perl を追加。eval() の仕様故に、若干心残りが…。残るは PHP と VBScript くらいか。




Scala: コンストラクタの意味合いとクロージャ

以前、「Scala: コンストラクタ中の一時オブジェクトに消えて欲しい – Ayutaya.com」などと言っていた私ですが、ちょっと考え違いをしていました。なまじ逆コンパイルなどをしてしまったために、C++~Java 的な構造体指向の発想にとらわれていましたが、文法的に見ると、Scala のクラス~コンストラクタは、静的なクロージャセットの構築だと見た方が自然です。であれば、クロージャに束縛された「スタック上の変数」が残るのも道理です。

例として、ベクトル型 (+ 絶対値を求めるメソッド) を Scala で下記のように書いたとして、

class Vect(x: Int, y: Int) {
  def abs = {
    math.sqrt(x * x + y * y)
  }
}

val v = new Vect(2, 3)
println(v.abs)

同じものを、例えば JavaScript で、プロトタイプを使わずにクロージャで書いたとします。

function Vect(x, y) {
  this.abs = function() {
    return (Math.sqrt(x * x + y * y));
  }
}

var v = new Vect(2, 3);
WScript.Echo(v.abs());

一目瞭然でそっくりです。Scala でオブジェクト生成の際に “new” が省略されないことも、それが意味するところが、通常の関数呼び出しと違って、クロージャの配列領域を確保する命令であると考えることができますし (上記の JavaScript でも “new” が、”this” に相当するオブジェクトを作っていますし)、パブリックな「メンバ変数」が実際にはアクセサとなっているところも、Scala のコンストラクタが「クロージャ群の構築子」であると考えれば一貫しています。

以上、Scala で腑に落ちた点をちょろっと。では。




Windows 7 で WSC をコマンドラインから登録する

WSC (Windows Script Components) を登録する際に、「右クリックして『登録』をクリック」がダサくて仕方がありません。複数マシンへ大量に登録したい時にどうしろってんだ、と。そこで、コマンドラインから登録できないものかと探していると、ありました、regsvr32.exe です。どうやら、OLE コントロールとしてひとくくりに、DLL や OCX の登録と同じ要領でよいようです。

# どうでもいいけど、あいかわらずマイクロソフトの、カタカナ単語間をスペースで切る表記はきもちわるい。”・”を使おうよ

当方 Windows 7 ですので、UAC (User Account Control) をパスするなり何なりして、管理者権限で実行する必要があるようです。

コマンドプロンプトを管理者権限で起動してから実行しても良いのですが、それもあまりスマートではありません。su(1) か sudo(8) のようなコマンドはないのですか? なに? runas.exe を使え? 了解しました。

> runas.exe /savecred /env /user:administrator "regsvr32.exe /s Foo.wsc"
administrator のパスワードを入力してください:

administrator アカウントのパスワード? そんなもの設定したおぼえがないよ。しかたがないので、administrator 権限を持つアカウントでコンソールを管理者権限で起動、パスワードを設定します。まあ、これは初回だけですし。

> net user administrator *
ユーザーのパスワードを入力してください:
確認のためにパスワードを再入力してください:
コマンドは正常に終了しました。

>

これで、こんどこそは行け…たのですが、何度かしくじっているうちに、以下のようになってしまいました。

> runas.exe /env /user:administrator "regsvr32.exe /s Foo.wsc"
regsvr32.exe /s Foo.wsc をユーザー "SVX\administrator" として開始して
います...
RUNAS エラー:  実行できません - regsvr32.exe /s /u AyutayaCalc.wsc
1327: ログオン失敗: ユーザー アカウントの制限。考えられる理由として、空
 のパスワードが許可されていない、ログオン時間制限、またはポリシーによる制
 限が適用された、などが挙げられます。
>

どうやら、パスワードなしの状態の時に適当なパスワードを入れてログオン認証に失敗しまくったせいで、アカウント停止を食らったようです。

> net user administrator | findstr /r アカウント有効
アカウント有効                       No
>

あらら。再度有効化します。

> net user administrator /active:yes
コマンドは正常に終了しました。

>

はい、何よりです。

面倒なので、”/env” で、administrator になった後も環境を引き継ぎます。これで、カレントディレクトリの “Foo.wsc” を登録できるようになります。su(1) で “-” をつけないような挙動ですね。”/savecred” で、安い Windows 以外ならばパスワードを記憶できるそうです。

> runas.exe /savecred /env /user:administrator "regsvr32.exe /s Foo.wsc"
administrator のパスワードを入力してください:
regsvr32.exe /s Foo.wsc をユーザー "SVX\administrator" として開始しています...
>

さて、JScript な WSH で WSC を書くとします。




ソース斜め読み: Eucalyptus Cloud Controller の Java コードのエントリはどこ?

表題の答えを先に書いておきますと、com.eucalyptus.bootstrap.SystemBootstrapper です。Eucalyptus のバージョンは、2.0.2 を見ています。

まずは、ソースツリーのおおまかな構成について見ておきます。Cluster Controller の本体は、C で書かれた Axis2/C のモジュール (libaEucalyptusCC.so) です。Apache + mod_axis2 配下で動きます。サブディレクトリは “cluster/” です。

Node Controller の本体も、C で書かれた Axis2/C のモジュール (libEucalyptusNC.so) で、Apache + mod_axis2 配下で動きます。ソースのサブディレクトリは “node/”。

問題は、それ以外――ソースのディレクトリでいう “clc/” 以下です。

追記 (Wed Feb 09 2011): eucalyptus-cloud 下の web サービス群 (ソース中では “component” と呼称される) は、Mule ESB バージョン 2.x で動作しています。別項で、いずれまた書きます。

Cloud Controller だけでなく、Walrus や Storage Controller は Java で書かれていて、呼び出し方からすると、複数機能を束ねた統一バイナリ (eucalyptus-cloud) のかたちをとっています。ところが、プロセスを見てみると、Java のものとおぼしき大量のスレッドはあるのですが、よく見かけるようには、オプションをごちゃごちゃつけて起動されている java コマンドが見あたりません。

$ pstree -alp
...
  |-eucalyptus-clou,28159 -h / -u eucalyptus --pidfile /var/run/eucalyptus ...
  |   `-eucalyptus-clou,28176 -h / -u eucalyptus --pidfile /var/run/eucaly ...
  |       |-bttrack,28514 /usr/bin/bttrack --port 6969 --dfile //var/lib/e ...
  |       |-{eucalyptus-clo},28193
  |       |-{eucalyptus-clo},28195
  |       |-{eucalyptus-clo},28202
...

ソースを見てみると、C でかかれた実行ファイル (eucalyptus-cloud) から、libjvm を経由して Java のコードを起動する作りになっています。しかたがないので clc/modules/bootstrap/eucalyptus-bootstrap.c の main() の定義から下って読んで行くと、fork(2) をして子プロセス (上の例で言う 28176) を作っています。子プロセスのその先の処理は、child()→java_init() で、JNI_CreateJavaVM() の呼び出しです。

JNI の呼び出しの詳細は、デバッグ出力で出るようになっています。eucalyptus-cloud コマンドに “–debug”、もしくは “–verbose” オプションをつけて呼び出せば、標準出力 (いや、エラー出力だったかな?) にデバッグ出力を出してくれます。Ubuntu-10.10 のパッケージだと upstart が出力を捨ててしまいますので、以下のようにして、適当なところにリダイレクトしておきます。

--- /etc/init/eucalyptus.conf.orig
+++ /etc/init/eucalyptus.conf
@@ -37,6 +37,7 @@
        . /etc/eucalyptus/eucalyptus.conf
        [ -n "$JVM_MEM" ] || JVM_MEM="512m"
        opts="-h $EUCALYPTUS -u $EUCA_USER --pidfile \
         /var/run/eucalyptus/eucalyptus.pid -l $LOGLEVEL -L console-log
        -Xmx$JVM_MEM"
+       opts="$opts --debug"
        services=""

        # If the -cloud package is not installed, disable the cloud service
@@ -72,7 +73,7 @@
        # Start the appropriate service(s)
        if [ -n "$services" ]; then
                # Cloud services to run
-               exec eucalyptus-cloud $opts
+               exec eucalyptus-cloud $opts > /tmp/eucalyptus.log 2>&1
        elif [ -r "/etc/init/eucalyptus-nc.conf" ] || \
         [ -r "/etc/init/eucalyptus-cc.conf" ]; then
                # Node or CC services to run
                # Why do we sleep here, rather than emitting a signal?
                #  Such that we can run:

すると、以下のような呼び出しが出ます (いい具合に改行を入れました)。

Using classpath: -Djava.class.path=//etc/eucalyptus/cloud.d:
 //etc/eucalyptus/cloud.d/scripts:
 //usr/share/eucalyptus/eucalyptus-component-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-storage-common-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-config-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-groupmgr-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-db-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-interface-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-walrus-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-storagecontroller-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-imagemgr-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-commons-ext-0.4.jar:
 //usr/share/eucalyptus/eucalyptus-www-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-auth-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-msgs-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-dns-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-core-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-clustermgr-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-cloud-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-keymgr-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-ws-2.0.0.jar:
 //usr/share/eucalyptus/eucalyptus-db-hsqldb-ext-2.0.0.jar:
 //usr/share/eucalyptus/wstx-lgpl.jar:
 //usr/share/eucalyptus/gnumail-providers.jar:
 //usr/share/eucalyptus/jetty-util.jar:
 //usr/share/eucalyptus/commons-logging.jar:
 //usr/share/eucalyptus/regexp.jar:
 //usr/share/eucalyptus/jcl-over-slf4j.jar:
 //usr/share/eucalyptus/drools-compiler.jar:
 //usr/share/eucalyptus/geronimo-jms-1.1-spec.jar:
 //usr/share/eucalyptus/commons-pool.jar:
 //usr/share/eucalyptus/geronimo-jpa-3.0-spec.jar:
 //usr/share/eucalyptus/axiom-dom.jar:
 //usr/share/eucalyptus/jibx-bind.jar:
 //usr/share/eucalyptus/commons-io.jar:
 //usr/share/eucalyptus/jetty-rewrite-handler.jar:
 //usr/share/eucalyptus/backport-util-concurrent.jar:
 //usr/share/eucalyptus/gnumail.jar:
 //usr/share/eucalyptus/wsdl4j.jar:
 //usr/share/eucalyptus/xercesImpl.jar:
 //usr/share/eucalyptus/servlet-api-2.5.jar:
 //usr/share/eucalyptus/commons-httpclient.jar:
 //usr/share/eucalyptus/euca_ipt:
 //usr/share/eucalyptus/slf4j-log4j12.jar:
 //usr/share/eucalyptus/commons-collections3.jar:
 //usr/share/eucalyptus/slf4j-api.jar:
 //usr/share/eucalyptus/chgrp-dhcpd:
 //usr/share/eucalyptus/geronimo-ejb-3.0-spec.jar:
 //usr/share/eucalyptus/dd-lv:
 //usr/share/eucalyptus/google-collections.jar:
 //usr/share/eucalyptus/activation.jar:
 //usr/share/eucalyptus/serializer.jar:
 //usr/share/eucalyptus/bcprov.jar:
 //usr/share/eucalyptus/jibx-run.jar:
 //usr/share/eucalyptus/jetty-sslengine.jar:
 //usr/share/eucalyptus/dom4j.jar:
 //usr/share/eucalyptus/axiom-impl.jar:
 //usr/share/eucalyptus/commons-discovery.jar:
 //usr/share/eucalyptus/hsqldb.jar:
 //usr/share/eucalyptus/jaxen.jar:
 //usr/share/eucalyptus/javassist.jar:
 //usr/share/eucalyptus/axiom-api.jar:
 //usr/share/eucalyptus/add_key.pl:
 //usr/share/eucalyptus/antlr.jar:
 //usr/share/eucalyptus/jaxp-1.3.jar:
 //usr/share/eucalyptus/chmod-dhcpd:
 //usr/share/eucalyptus/geronimo-interceptor-3.0-spec.jar:
 //usr/share/eucalyptus/proxool.jar:
 //usr/share/eucalyptus/commons-logging-adapters.jar:
 //usr/share/eucalyptus/hsqldbutil.jar:
 //usr/share/eucalyptus/jibx-extras.jar:
 //usr/share/eucalyptus/geronimo-jta-1.0.1b-spec.jar:
 //usr/share/eucalyptus/jetty.jar:
 //usr/share/eucalyptus/gwt-servlet.jar:
 //usr/share/eucalyptus/xpp3.jar:
 //usr/share/eucalyptus/jul-to-slf4j.jar:
 //usr/share/eucalyptus/commons-cli.jar:
 //usr/share/eucalyptus/xalan2.jar:
 //usr/share/eucalyptus/commons-beanutils.jar:
 //usr/share/eucalyptus/mvel.jar:
 //usr/share/eucalyptus/janino.jar:
 //usr/share/eucalyptus/commons-fileupload.jar:
 //usr/share/eucalyptus/commons-logging-api.jar:
 //usr/share/eucalyptus/junit.jar:
 //usr/share/eucalyptus/geronimo-j2ee-connector-1.5-spec.jar:
 //usr/share/eucalyptus/ant.jar:
 //usr/share/eucalyptus/gwt-user.jar:
 //usr/share/eucalyptus/commons-codec.jar:
 //usr/share/eucalyptus/wss4j.jar:
 //usr/share/eucalyptus/bsf.jar:
 //usr/share/eucalyptus/commons-lang.jar:
 //usr/share/eucalyptus/bcel.jar:
 //usr/share/eucalyptus/excalibur-logkit.jar:
 //usr/share/eucalyptus/netty.jar:
 //usr/share/eucalyptus/el-api-2.1.jar:
 //usr/share/eucalyptus/drools-core.jar:
 //usr/share/eucalyptus/cglib.jar:
 //usr/share/eucalyptus/populate_arp.pl:
 //usr/share/eucalyptus/inetlib.jar:
 //usr/share/eucalyptus/jug-asl.jar:
 //usr/share/eucalyptus/ezmorph.jar:
 //usr/share/eucalyptus/groovy.jar:
 //usr/share/eucalyptus/commons-jxpath.jar:
 //usr/share/eucalyptus/log4j-1.2.jar:
 //usr/share/eucalyptus/xml-security.jar:
 //usr/share/eucalyptus/xom.jar:
 //usr/share/eucalyptus/modprobe-aoe:
 //usr/share/eucalyptus/dnsjava.jar:
 //usr/share/eucalyptus/ecj.jar:
 //usr/share/eucalyptus/json-lib.jar

Version:                       10004
Ignore Unrecognized Arguments: 0
Extra options:                 33
  "-Xbootclasspath/p://usr/share/eucalyptus/openjdk-crypto.jar    " (0x(nil))
  "-Xmx512m                                                       " (0x(nil))
  "-XX:MaxPermSize=128m                                           " (0x(nil))
  "-XX:+UseConcMarkSweepGC                                        " (0x(nil))
  "-Djava.net.preferIPv4Stack=true                                " (0x(nil))
  "-Djava.security.policy=//etc/eucalyptus/cloud.d/security.policy" (0x(nil))
  "-Djava.library.path=//usr/lib/eucalyptus                       " (0x(nil))
  "-Dsun.java.command=Eucalyptus                                  " (0x(nil))
  "-Deuca.home=//                                                 " (0x(nil))
  "-Deuca.var.dir=//var/lib/eucalyptus                            " (0x(nil))
  "-Deuca.run.dir=//var/run/eucalyptus                            " (0x(nil))
  "-Deuca.lib.dir=//usr/share/eucalyptus                          " (0x(nil))
  "-Deuca.conf.dir=//etc/eucalyptus/cloud.d                       " (0x(nil))
  "-Deuca.log.dir=//var/log/eucalyptus                            " (0x(nil))
  "-Deuca.version=2.0.0                                           " (0x(nil))
  "-Deuca.log.exhaustive.db=FATAL                                 " (0x(nil))
  "-Deuca.log.exhaustive.cc=FATAL                                 " (0x(nil))
  "-Deuca.log.exhaustive.user=FATAL                               " (0x(nil))
  "-Deuca.log.exhaustive.external=FATAL                           " (0x(nil))
  "-Deuca.log.level=DEBUG                                         " (0x(nil))
  "-Deuca.log.appender=console-log                                " (0x(nil))
  "-Deuca.db.port=9001                                            " (0x(nil))
  "-Deuca.db.host=127.0.0.1                                       " (0x(nil))
  "-Deuca.walrus.host=localhost                                   " (0x(nil))
  "-Deuca.remote.dns=true                                         " (0x(nil))
  "-Xdebug                                                        " (0x(nil))
  "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005  " (0x(nil))
  "-Dcom.sun.management.jmxremote                                 " (0x(nil))
  "-XX:+HeapDumpOnOutOfMemoryError                                " (0x(nil))
  "-XX:HeapDumpPath=//var/log/eucalyptus/                         " (0x(nil))
  "-Xmx512m                                                       " (0x(nil))
  "-Djava.class.path=//etc/eucalyptus/cloud.d://etc/eucalyptus/..." (0x(nil))
  "abort                                                          " (0x0x4018c0)

バラバラとパラメータを渡しています。おケツの “abort” の追加情報 0x4018C0 は、abort の際のコールバック関数ですね。表示が変ですが。

static void java_fail(void) { exit(1); }

int java_init(euca_opts *args, java_home_t *data) {
  ...
  opt[++x].optionString="abort";
  opt[x].extraInfo=java_fail;

エントリの jar ファイルも指定していませんので、ここではまだ起動はしません。単に、さっきの “JNI_CreateJavaVM” で、VM を作るだけです。かなめは、さきほどの java_load_bootstrapper() です。以下のマクロで別名ですので、euca_load_bootstrapper() を探します。

#define java_load_bootstrapper euca_load_bootstrapper

euca_load_bootstrapper() では、構造体インスタンス “bootstrap” を作ります。これが Java コードのエントリポイントになります。まず、クラスの取得です。

#define EUCA_MAIN "com/eucalyptus/bootstrap/SystemBootstrapper"

bootstrap.clazz=((*env)->FindClass(env,EUCA_MAIN))

次に、static なファクトリメソッドを呼び出して、ブートストラップのインスタンスを作ります。

bootstrap.constructor=(*env)->GetStaticMethodID(env, \
 bootstrap.clazz,euca_get_instance.method_name, \
 euca_get_instance.method_signature );

この後は、ここから始まる Java コードの仕事ですね。続きは後ほど。

# なぜにこうもツギハギ感溢れる作りになってるんでしょうね? Eucalyptus は

今なら JNI よりも、JNA というのがありますよー、と同僚に紹介されました。後で読む。

JNIより簡単にJavaとC/C++をつなぐ「JNA」とは(1/4)-@IT




私家版 Oracle Cheat Sheet

理解はしていても忘れてしまうが、見れば思い出す断片の羅列です。

PL/SQL

リンク: Oracle Database PL/SQLユーザーズ・ガイドおよびリファレンス 10.2

SQL*Plus の操作
  • $ sqlplus ID/PASS@HOST:1521/SID
  • コンソール出力をオンに: “set serveroutput on
  • ファイルの読み込み実行: “@C:\priv\2010\test.plsql
  • 直前のエラーの表示: “show errors
  • コメント /* コメント */, — コメント
    配列
  • int index な map
  • メソッド: count … 歯抜けで存在する数を返す, delete … 全て, delete(i) … Pascal 風 1 から添字, delete(i, j) … j は末尾自体なので注意, first … 以下は添字を返す, last, prior(i), next(i), exists(i)
  • コード断片

    諸々:

    -- Oracle Database PL/SQLユーザーズ・ガイドおよびリファレンス 10.2
    
    drop table foo;
    create table foo(k integer, v varchar2(30));
    insert into foo values(1, 'first');
    insert into foo values(2, 'second');
    insert into foo values(3, 'third');
    
    declare
      n1 number not null default 100;
      A$_#09 constant number := 1000; -- ID に利用可能な文字
      s1 char(3) := 'abc';
      type rectag is record (
        n number(3) default 200,
        s nvarchar2(30) := 'こんにちは' -- サイズ省略不可
      );
      rec rectag;
      type tabtag is table of rectag index by pls_integer;
      tab tabtag;
      type nestedtag is table of foo%rowtype;
      nested nestedtag := nestedtag();
      -- i pls_integer; -- 不要: ループ変数は暗黙で
    begin
      dbms_output.put_line('d0: ' || n1 || ', ' || s1);
      -- rec := tab(1); -- 参照しても生成されません
      tab(1).n := 300;
      for i in 2 .. 3 loop
        tab(i).s := 'さようなら';
      end loop;
      for i in 1 .. tab.count loop -- 他に loop, while ~ loop
        dbms_output.put_line('d0: ' || tab(i).n || ', ' || tab(i).s);
      end loop;
      if n1 < 0 then
        dbms_output.put_line('d1');
      elsif 0 <= n1 and n1 < 100 then
        dbms_output.put_line('d2');
      else
        dbms_output.put_line('d3');
      end if;
      case n1 /* 検索子式 */
        when 100 then
          dbms_output.put_line('d4');
        else
          dbms_output.put_line('d5');
      end case;
      case -- 検索 case 文
        when n1 = 100 then
          dbms_output.put_line('d6');
        else
          null;
      end case;
      declare /* ブロック静的スコープ */
        /* rec0 foo%rowtype; -- 不要: ループ変数は暗黙 */
        cursor cur0(arg number) is
         select * from foo where k >= arg for update;
         -- パラメータ付き cursoer
        n number;
      begin
        /* open cur0; -- エラー: 明示 open ならば fetch cur into rec0 */
        -- n := amp;tmp;
        n := 2;
        for rec0 in cur0(n) loop
          if rec0.k = 2 then
            update foo set k = k * 2 where current of cur0;
             -- "current of CURSOR" が使えるのは for update にだけ
          end if;
          if cur0%isopen then
           -- カーソル属性: %notfound, %found, %rowcont, %isopen
            dbms_output.put_line('cp0: is open');
          else
            dbms_output.put_line('cp1: not open');
          end if;
          dbms_output.put_line(rec0.k); -- これは当然、変更前の値
        end loop;
        open cur0(2);
        if cur0%isopen = true then
          dbms_output.put_line('cp1: is open');
        else
          dbms_output.put_line('cp1: not open');
        end if;
      end;
      nested.extend(10);
      nested(1).k := 500;
      nested(1).v := 'five hundred';
      raise_application_error(-20001, 'ほげほげ');
    exception
      /* 10 PL/SQLエラーの処理/事前定義のPL/SQL例外のまとめ */
      when no_data_found then
        dbms_output.put_line('error 0: ' || sqlcode || ', ' || sqlerrm);
      when others then
        dbms_output.put_line('error 1: ' || sqlcode || ', ' || sqlerrm);
    end;
    /
    

    トリガ:

    drop table foo;
    create table foo(k integer, v varchar2(30));
    
    create or replace trigger foo_trigger
     before insert or update on foo
     for each row
     when (new.k is not null)
    begin
      if inserting or updating then
        :new.v := :new.v || ' foo';
      end if;
    end;
    /
    
    insert into foo values(1, 'first');
    insert into foo values(2, 'second');
    insert into foo values(3, 'third');
    
    select * from foo;
    



    私家版 C++ Cheat Sheet

    理解はしていても忘れてしまうが、見れば思い出す断片の羅列です。

    真偽値 bool で true, false

    Standard Library

    リンク: vector – C++ Reference

    string
  • コンストラクタ: string(), string(string), string(string, pos, len = to_the_end), string(sz, len = to_the_end), string(n, ch), string(i, j)
  • メソッド: const char * c_str(), const char * data(), find(str, pos = 0), find(sz, pos = 0), find(ch, pos = 0) … pos は対象の位置, rfind(…), string substr(pos = 0, len = to_the_end), compare(), copy(szDst, len, pos = 0) … \0 は付かない
  • その他: operator<<(os, const & str), operator>>(is, & str), operator+(& str と ch, sz, psz の 2 項)
  • シーケンス共通
  • コンストラクタ: vector(), vector(n, x), vector(i, j), vector(v) … コピコン
  • メソッド: size(), empty(), swap(v), insert(i, x) … i の前に入る, insert(i, n, x), insert(i, iSrc, jSrc)? … もちろんコピーが入る, erase(i) ⇒ i の次, erase(i, j) … [i, j) を削除 ⇒ j の次, clear(), assign(i, j), assign(n, x), max_size(), pop_back(x), push_back(x), resize(n, x), get_allocator()
  • イテレータbegin(), end(), rbegin(), rend(), front() ⇒ begin(), back() ⇒ end() - 1
  • ::value_type, ::pointer, ::const_pointer, ::reference, ::const_reference, ::iterator, ::const_iterator, ::const_reverse_iterator, ::reverse_iterator, ::size_type, ::difference_type, ::allocator_type
  • vector
  • メソッド: ランダムアクセスで at(ind), operator[](ind). 拡大時要全コピーなので capacity(), reserve(n)
  • deque
  • ランダムアクセスで at(ind), operator[](ind). フロント O(1) で push_front(x), pop_front()
  • list
  • merge, remove, remove_if, reverse, pop_front, push_front, sort, splice, unique
  • set, multiset
  • set(cmp), set(i, j, cmp), set(s)
  • empty(), size(), max_size(), insert(x), insert(i, x) … i はヒント, insert(i, j), erase(i), erase(x), erase(i, j), clear(), swap(s), key_comp(), value_comp(), get_allocator()
  • find(x), count(x) ⇒ set ならば 0 か 1, lower_bound(x), upper_bound(x), pair(itr, itr> equal_range(x)
  • begin(), end(), rbegin(), rend()
  • map, multimap
  • map(cmp), map(i, j, cmp), map(m)
  • m[key] = val, i->second = val, 右辺 m[ind] ⇒ 無ければ新規 pair(key, val)
  • アルゴリズム
  • non-modifying: find(i, j, x), find_if(i, j, cmp) … 1stのみ, count(i, j, x), count_if(i, j, cmp), adjacent_find(i, j), adjacent_find(i, j, cmp), for_each(i, j, f) ⇒ f, bool mismatch(i, j, iAnother), bool equal(i, j, iAnother), bool search(iHay, jHay, iNdl, jNdl),
  • modifying: copy(i, j, iDest) ⇒ コピー先の尻の次, copy_backward(i, j, jDest) ⇒ コピー先の頭, fill(i, j, x), fill(i, n, x), generate(i, j, f) … f に引数は無い, partition(i, j, f) ⇒ 後半 ! f の頭, random_shuffle(i, j), erase(remove(v.begin(), v.end(), x), v.end()), replace(i, j, x, y), reverse(i, j), rotate(i, k, j) … [i, j) の k が新しい頭になる, swap(cnt1, cnt2), swap_ranges(i, j, iOther), transform(…), unique(i, j) … 返り値は remove() と同方式,
  • ソート:
  • 関数 obj
  • bind1st, bind2nd



  • 私家版 JavaScript Cheat Sheet

    Warning: include(/var/www/wordpress//home/knaka/pub/inc/javascript-cheat.html): failed to open stream: No such file or directory in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102 Warning: include(): Failed opening '/var/www/wordpress//home/knaka/pub/inc/javascript-cheat.html' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102




    私家版 Python Cheat Sheet

    Warning: include(/var/www/wordpress//home/knaka/pub/inc/python-cheat.html): failed to open stream: No such file or directory in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102 Warning: include(): Failed opening '/var/www/wordpress//home/knaka/pub/inc/python-cheat.html' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102




    私家版 Scala Cheat Sheet

    Warning: include(/var/www/wordpress//home/knaka/pub/inc/scala-cheat.html): failed to open stream: No such file or directory in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102 Warning: include(): Failed opening '/var/www/wordpress//home/knaka/pub/inc/scala-cheat.html' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /var/www/wordpress/wp-content/plugins/include-it/plugin.php on line 102




    Scala: 中置記法→逆ポーランド記法のパーサ練習

    コップ本の第 31 章「パーサー・コンビネーター」の初っ端のサンプルです。とりあえず、いきなり Scala によるパーサジェネレータの記述方法を見せられたらビビると思われます。私はビビった。

    import scala.util.parsing.combinator._
    object Arith extends JavaTokenParsers {
      def expr: Parser[Any] = term ~ rep("+" ~ term | "-" ~ term)
      def term: Parser[Any] = factor ~ rep("*" ~ factor | "/" ~ factor)
      def factor: Parser[Any] = floatingPointNumber | "(" ~ expr ~ ")"
    }
    

    実行してみます。

    scala> Arith.parseAll(Arith.expr, "1 + 2 - 3 * 4 / (5 + 6)")
    res9: Arith.ParseResult[Any] =
     [1.24] parsed: ((1~List())~List((+~(2~List())),
     (-~(3~List((*~4), (/~(((~((5~List())~List((+~(6~List())))))~))))))))
    
    scala>
    

    良いようですが、省略記法の使いすぎで、パッと見、何をやっているのかがさっぱり分かりません。省略せずに、書き下してみます。

    import scala.util.parsing.combinator._
    object Arith extends JavaTokenParsers {
      def expr: Parser[Any] =
       term.~(rep((literal("+").~(term)).|(literal("-").~(term))))
      def term: Parser[Any] =
       factor.~(rep((literal("*").~(factor)).|(literal("/").~(factor))))
      def factor: Parser[Any] =
       floatingPointNumber.|(literal("(").~(expr).~(literal(")")))
    }
    

    パーサ間で「演算」を繰り返して、新しいパーサを構築していくプロセスが見えます (いや、あいかわらず分かりづらいのは同じですが)。実行してみます。

    scala> Arith.parseAll(Arith.expr, "1 + 2 - 3 * 4 / (5 + 6)")
    res10: Arith.ParseResult[Any] =
     [1.24] parsed: ((1~List())~List((+~(2~List())),
     (-~(3~List((*~4), (/~(((~((5~List())~List((+~(6~List())))))~))))))))
    
    scala> res9.toString == res10.toString
    res11: Boolean = true
    
    scala>
    

    以上の例では、生成されたパーサが変換処理を含まないので、出力はデフォルトの垂れ流しで、分かりづらいです。そこで、練習がてら、中置記法から逆ポーランド記法への変換パーサのジェネレータに修正してみます。

    import scala.util.parsing.combinator._
    object Arith extends JavaTokenParsers {
      def expr: Parser[String] = term ~ rep("+" ~ term | "-" ~ term) ^^ {
       case term ~ rest => reduce(term, rest) }
      def term: Parser[String] = factor ~ rep("*" ~ factor | "/" ~ factor) ^^ {
       case factor ~ rest => reduce(factor, rest) }
      def reduce(x: String, pairs: List[~[String, String]]) =
        (x /: pairs) ((x, pair) => "%s %s %s".format(x, pair._2, pair._1))
      def factor: Parser[String] = floatingPointNumber | "(" ~> expr <~ ")"
    }
    

    実行してみます。

    scala> Arith.parseAll(Arith.expr, "1 + 2 - 3 * 4 / (5 + 6)")
    res12: Arith.ParseResult[String] = [1.24] parsed: 1 2 + 3 4 * 5 6 + / -
    
    scala>
    

    この機能があれば、外部 DSL のパーサが、外部ツールの助けなしにチョチョイのチョイで書けてしまうわけですね。すごいですねぇ…。




    Scala: コンストラクタ中の一時オブジェクトに消えて欲しい

    Scala については、まだまだ学習中の身ですが、このへんを越えないと安心できない気がするので、書きます。

    Scala クラスの基本コンストラクタ (primary constructor) では、クラス定義、コンストラクタ定義、フィールド定義が一緒くたに書けるので、大抵の場合は既存の OOP 言語のそれらと比べて、記述がスッキリして、とても良いです。

    class Point(val x: Int, val y: Int)
    val p = new Point(3, 4)
    println(p.x + ", " + p.y) // 3, 4
    

    ところが、コンストラクタ中で一時変数が必要になると、何やら気持ち悪いことになります。

    例としては、「コップ本」こと「Scala スケーラブルプログラミング」から。コップ本の最初の OOP の例で、有理数をとりあげています。最大公約数を求めて分母・分子を約分するのですが、基本コンストラクタ内で求めた最大公約数が、このクラスのインスタンスが存続する限りフィールドとして残ってしまいます、もう不要なのに。

    class Rational (
      numArg: Int,
      denArg: Int ) {
      def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
      private val n = gcd(numArg.abs, denArg.abs) // GCD を計算
      val num = numArg / n
      val den = denArg / n
      // private だからアクセスはできないものの、この後、n は永続
    }
    val r = new Rational(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    うーん…。

    なお、基本コンストラクタの引数は、普通に記述すると、インスタンスの private なフィールドになりますが、val や var で宣言すると、値は隠しフィールドに代入され、そこへのアクセサが自動生成されます (「パラメータフィールド」と呼ぶそうです)。どうやら、Scala のアクセスコントロールが JRE のそれよりも細かいので、アクセサで何とかしているようですよ。多分。下記参照:

    間違ってたら直します。

    追記 (Wed Oct 27 2010): よく考えたら、overide や lazy にも必要ですね。

    それならばと、分子・分母を reader にすれば、n は不要にはならないので、気分的には若干マシになります。けれども、何の解決にもなっていません。

    class Rational (
      numArg: Int,
      denArg: Int ) {
      def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
      private val n = gcd(numArg.abs, denArg.abs)
      def num = numArg / n // 呼び出し時の評価…
      def den = denArg / n
    }
    val r = new Rational(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    どうしたものかといろいろやっていましたが、当然すでに、いろいろと議論があります。

    上記を参考にしつつ、いろいろやってみます。要は、一時変数をスタック上に確保できれば良いのです。

    まずは、基本 constructor を private にして、代替 constructor から呼ぶことを考えましたが…、残念、通りません。Scala の代替 constructor では、まず最初に基本 constructor を呼ばなければならないルールになっています。インスタンスの初期化以前にいろいろやるな、というお達しでしょうが、厳しいです。

    class Rational private ( // コンパイルが通りません
      val num: Int,
      val den: Int,
      dummy: Unit ) {
      def this(num: Int, den: Int) = {
        def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
        val n = gcd(num.abs, den.abs)
        this(num / n, den / n, ())
      }
    }
    

    次に、コンパニオンオブジェクトで、ファクトリメソッドを用意します。apply にしたところで、話は同じ。これは悪くなさそうですが、初期化引数の正規化は、ファクトリではなくコンストラクタの仕事にしたいかなぁ…。インスタンス生成のしかたも変わってしまいますし。

    class Rational(val num: Int, val den: Int)
    object Rational {
      def create(num: Int, den: Int) = {
        def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
        val n = gcd(num.abs, den.abs)
        new Rational(num / n, den / n)
      }
    }
    val r = Rational.create(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    それではと、フィールドの生成に、タプルを使ってみます。一見良いものの、今度は Tuple2 が private フィールドに残ります (自動的に付与される名前の “x$1″ とかで、アクセスできたりする。Scala 的には、メンバとしての位置づけなんだろう)。ですので、かえってデカいんじゃ?

    class Rational (
      numArg: Int,
      denArg: Int ) {
      val (num, den) = {
        val n = gcd(numArg.abs, denArg.abs)
        (numArg / n, denArg / n)
      }
      private def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
    }
    val r = new Rational(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    逆アセンブルすると、以下のような具合です:

    $ javap -c -private Rational
    Compiled from "Rational.scala"
    public class Rational extends java.lang.Object implements scala.ScalaObject{
    private final int den;
    
    private final int num;
    
    private final scala.Tuple2 x$1;
    
    public Rational(int, int);
      Code:
    (中略)
    $
    

    これで最後。いろいろやってみましたが、一時変数が lexical scope 内に入るので、これが一番綺麗かな? フィールドを、private とはいえ var にせざるを得ないので、アクセスコントロール (リードオンリー) は accessor で行ないます。ブロックを、単なるブレースで括ろうとすると、ここでは print からの戻り値への行継続で、関数の引数と解釈されてコンパイルがコケますので、do-while-false ループ (C 言語で、複文を、値を返さない式の位置に書く際に使うイディオム) にしてみます (素直にセミコロンを置いても良いのですが、カッコ悪いので)。

    class Rational (
      private var numField: Int, // 仕方がないので var にします
      private var denField: Int ) {
      print("Initializing ...")
      do { // C でよくやるループ
        val n = gcd(numField, denField) // これはスタック上に確保される
        numField /= n // フィールド変数へ再代入 (実際はアクセサ)
        denField /= n
      } while (false) // 最適化されるので、ループにはならない (多分…)
      println(" Done.")
      def num = numField // public かつ read-only
      def den = denField
      def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
    }
    val r = new Rational(4, 6)
    println(r.num + ", " + r.den) // 2, 3
    

    一応、do-while-false がループになっていないことと、num と den がリードオンリーであることと、numField と denField へのアクセスが private のアクセサを経由していることを確認します。

    $ javap -c -private Rational
    Compiled from "Rational.scala"
    public class Rational extends java.lang.Object implements scala.ScalaObject{
    private int denField;
    
    private int numField;
    
    public Rational(int, int);
     Code:
      0:  aload_0
      1:  iload_1
      2:  putfield       #13; //Field numField:I
      5:  aload_0
      6:  iload_2
      7:  putfield       #15; //Field denField:I
      10: aload_0
      11: invokespecial  #20; //Method java/lang/Object."<init>":()V
      14: getstatic      #26; //Field scala/Predef$.MODULE$:Lscala/Predef$;
      17: ldc            #28; //String Initializing ...
      19: invokevirtual  #32; //Method scala/Predef$.print:(Ljava/lang/Object;)V
      22: aload_0
      23: iload_1
      24: iload_2
      25: invokevirtual  #36; //Method gcd:(II)I
      28: istore_3
      29: aload_0
      30: iload_1
      31: iload_3
      32: idiv
      33: invokespecial  #40; //Method numField_$eq:(I)V
      36: aload_0
      37: iload_2
      38: iload_3
      39: idiv
      40: invokespecial  #43; //Method denField_$eq:(I)V
      43: getstatic      #26; //Field scala/Predef$.MODULE$:Lscala/Predef$;
      46: ldc            #45; //String  Done.
      48: invokevirtual  #48; //Method scala/Predef$.println:(Ljava/lang/Object;)V
      51: return
    
    (中略)
    
    public int den();
      Code:
       0: aload_0
       1: invokespecial  #58; //Method denField:()I
       4: ireturn
    
    public int num();
      Code:
       0: aload_0
       1: invokespecial  #61; //Method numField:()I
       4: ireturn
    
    private void denField_$eq(int);
      Code:
       0: aload_0
       1: iload_1
       2: putfield       #15; //Field denField:I
       5: return
    
    private int denField();
      Code:
       0: aload_0
       1: getfield       #15; //Field denField:I
       4: ireturn
    
    private void numField_$eq(int);
      Code:
       0: aload_0
       1: iload_1
       2: putfield       #13; //Field numField:I
       5: return
    
    private int numField();
      Code:
       0: aload_0
       1: getfield       #13; //Field numField:I
       4: ireturn
    
    (中略)
    
    }
    
    $
    

    Scala は、総じては超ステキ言語なのですが、時たま、策士が策に溺れている感があります。そんなところも大好きですが。




    Fedora の Scala の RPM を CentOS でビルド

    Scala とだったら、JRE とうまくやっていけるんじゃないかという予感がしています。

    Fedora では、yum 一発で入ってくれるので助かります。

    [root@fedora ~]# yum install scala
    [root@fedora ~]#
    

    一方の CentOS-5.5 ですが、Scala が yum レポジトリ上にないのもさることながら、さすがに Java 関連のモジュールがいいかげん古くて、ビルドもままなりません。仕方ないので、Fedora のソースからビルドします。下記の各パッケージの情報ページ上部の “Build” の先から、新しい目の source rpm をいただいてきます:

    バージョンの上下でモグラ叩きになりますが、どうにか以下のような感じで順にビルドし、インストールします。ant は最低でも 1.7 でないと、scala のコンパイルに失敗します。よく見ていませんが、scala-2.8.0 は document まわりのコンパイルにコケていたので、2.7.7 を利用しています。充分ですよね?

    ant-contrib だけは EPEL のレポジトリから入れます。手順の最後に、標準以外のレポジトリから入れている ant-contrib と、標準パッケージと競合する ant-1.8 は remove しています。

    なお、下記は root による 実行例ですが、一般論として特権ユーザによる RPM の作成は危険ですので、普通は ~/.rpmmacros を用意して、一般ユーザで実行しましょう。それと、例によって Fedora の RPM キーは何か壊れているようで、MD5 のチェックが通りません。インストールには “–nomd5″ オプションが必要になっています。

    [root@node1 ~]# yum install -y rpm-build
    [root@node1 ~]# host=kojipkgs.fedoraproject.org
    [root@node1 ~]# wget \
     http://$host/packages/shtool/2.0.8/4.fc14/src/shtool-2.0.8-4.fc14.src.rpm \
     http://$host/packages/jline/0.9.94/0.6.fc14/src/jline-0.9.94-0.6.fc14.src.rpm \
     http://$host/packages/ant/1.8.1/6.fc15/src/ant-1.8.1-6.fc15.src.rpm \
     http://$host/packages/scala/2.7.7/1.fc13/src/scala-2.7.7-1.fc13.src.rpm
    [root@node1 ~]# rpm -i --nomd5 \
     shtool-2.0.8-4.fc14.src.rpm \
     jline-0.9.94-0.6.fc14.src.rpm \
     ant-1.8.1-6.fc15.src.rpm \
     scala-2.7.7-1.fc13.src.rpm
    [root@node1 ~]# topdir=/usr/src/redhat/
    [root@node1 ~]# rpmbuild -ba $topdir/SPECS/shtool.spec
    [root@node1 ~]# rpm -Uvh $topdir/RPMS/noarch/shtool-2.0.8-4.noarch.rpm
    [root@node1 ~]# yum install -y ant junit
    [root@node1 ~]# rpmbuild --without maven -ba $topdir/SPECS/jline.spec
    [root@node1 ~]# rpm -Uvh $topdir/RPMS/noarch/jline-0.9.94-0.6.noarch.rpm
    [root@node1 ~]# perl -p -i -e 's/(jpackage-utils) >= .*/\1/' \
     $topdir/SPECS/ant.spec
    [root@node1 ~]# rpmbuild --with bootstrap --without gcj_support \
     -ba $topdir/SPECS/ant.spec
    [root@node1 ~]# rpm -Uvh $topdir/RPMS/noarch/ant-1.8.1-6.noarch.rpm \
     $topdir/RPMS/noarch/ant-nodeps-1.8.1-6.noarch.rpm
    [root@node1 ~]# rpm -Uvh $(printf \
     ftp://download.fedora.redhat.com/pub/epel/%s/%s/epel-release-*-*.noarch.rpm \
     $(rpm -q --qf "%{version}" $(rpm -q --whatprovides redhat-release)) \
     $(uname --hardware-platform) )
    [root@node1 ~]# yum install -y ant-contrib
    [root@node1 ~]# rpmbuild -ba -D "fedora %nil" $topdir/SPECS/scala.spec
    [root@node1 ~]# yum remove -y ant ant-nodeps epel-release
    [root@node1 ~]# rpm -Uvh $topdir/RPMS/noarch/scala-2.7.7-1.noarch.rpm \
     $topdir/RPMS/noarch/scala-examples-2.7.7-1.noarch.rpm
    [root@node1 ~]# scala -e \
     'println(List("foo", "hoge").map((s: String) => s.length).mkString("\n"))'
    3
    4
    [root@node1 ~]#
    

    一度ビルドできれば、次からはバイナリだけ入れれば OK でしょう。SRPM 無修正という縛りプレイで行こうとしたのですが、残念なことに、jpackage-utils のバージョンだけ、静的に修正しています。

    Sun JDK の手順も、後で試しておきます。Hadoop の絡みで、OpenJDK では行けないケースもありますので。




    PostgreSQL + Slony-I: 3 台構成

    PostgreSQL 用の、トリガベースのレプリケーションシステムである Slony-I は、非同期でありながらクラスタ全体がインテリジェントに連動して動く素晴らしいシステムなのですが、いかんせん敷居が高いです。構築の定形パターンを作っておこうと思ったので、メモに残します。

    PostgreSQL のインストール、設定、起動

    PostgreSQL のインストールをします。

    [root@node1 ~]# yum install -y postgresql84 postgresql84-server
    (中略)
    Installed:
      postgresql84.x86_64 0:8.4.4-1.el5_5.1
      postgresql84-server.x86_64 0:8.4.4-1.el5_5.1
    
    Dependency Installed:
      postgresql84-libs.x86_64 0:8.4.4-1.el5_5.1
    
    Complete!
    [root@node1 ~]#
    

    データベースを初期化、起動し、システム起動時に起動するようにします。

    [root@node1 ~]# service postgresql initdb
    データベースを初期化中:                                    [  OK  ]
    [root@node1 ~]# service postgresql start
    postgresql サービスを開始中:                               [  OK  ]
    [root@node1 ~]# chkconfig postgresql on
    [root@node1 ~]#
    

    CentOS のデフォルトだと、PostgreSQL は ident 認証になっていますので、”postgres” ユーザに su します。とりあえずロケールと文字エンコーディングを確認し、リモートからアクセスできるようにしておきます。pg_hba.conf のアクセス制御のネットワークの指定については、環境ごとに適宜読み替えてください。

    [root@node1 ~]# su - postgres
    -bash-3.2$ psql template1 -c "select datname, datcollate,
     datctype, pg_encoding_to_char(encoding) from pg_database"
      datname  | datcollate  |  datctype   | pg_encoding_to_char
    -----------+-------------+-------------+---------------------
     template1 | ja_JP.UTF-8 | ja_JP.UTF-8 | UTF8
     template0 | ja_JP.UTF-8 | ja_JP.UTF-8 | UTF8
     postgres  | ja_JP.UTF-8 | ja_JP.UTF-8 | UTF8
    (3 行)
    
    -bash-3.2$ cp /var/lib/pgsql/data/postgresql.conf \
     /var/lib/pgsql/data/postgresql.conf.orig
    -bash-3.2$ vi /var/lib/pgsql/data/postgresql.conf
    -bash-3.2$ diff -uNr /var/lib/pgsql/data/postgresql.conf.orig \
     /var/lib/pgsql/data/postgresql.conf
    --- /var/lib/pgsql/data/postgresql.conf.orig    2010-09-28 16:58:35.000000000 +0900
    +++ /var/lib/pgsql/data/postgresql.conf 2010-09-28 17:01:38.000000000 +0900
    @@ -56,7 +56,7 @@
    
     # - Connection Settings -
    
    -#listen_addresses = 'localhost'                # what IP address(es) to listen on;
    +listen_addresses = '*'         # what IP address(es) to listen on;
                                            # comma-separated list of addresses;
                                            # defaults to 'localhost', '*' = all
                                            # (change requires restart)
    -bash-3.2$ cp /var/lib/pgsql/data/pg_hba.conf \
     /var/lib/pgsql/data/pg_hba.conf.orig
    -bash-3.2$ vi /var/lib/pgsql/data/pg_hba.conf
    -bash-3.2$ diff -uNr /var/lib/pgsql/data/pg_hba.conf.orig \
     /var/lib/pgsql/data/pg_hba.conf
    --- /var/lib/pgsql/data/pg_hba.conf.orig        2010-09-28 17:09:08.000000000 +0900
    +++ /var/lib/pgsql/data/pg_hba.conf     2010-09-28 17:09:51.000000000 +0900
    @@ -72,3 +72,5 @@
     host    all         all         127.0.0.1/32          ident
     # IPv6 local connections:
     host    all         all         ::1/128               ident
    +# IPv4 local network connections:
    +host    postgres    postgres    192.168.1.0/24        md5
    -bash-3.2$ exit
    logout
    [root@node1 ~]# service postgresql restart
    postgresql サービスを停止中:                               [  OK  ]
    postgresql サービスを開始中:                               [  OK  ]
    [root@node1 ~]# iptables -I INPUT -p tcp --dport 5432 -j ACCEPT
    [root@node1 ~]# service iptables save
    ファイアウォールのルールを /etc/sysconfig/iptables に保存中[  OK  ]
    [root@node1 ~]#
    

    サンプルデータの作成

    ある程度 1 台で運用していたと見立てて、テスト用のテーブルとデータを作成します。

    [root@node1 ~]# su - postgres
    -bash-3.2$ psql -c "
     create table foo (k integer primary key, v integer);
     insert into foo (
                select 1, (random() * 100)::integer
      union all select 2, (random() * 100)::integer
      union all select 3, (random() * 100)::integer )"
    NOTICE:  CREATE TABLE / PRIMARY KEYはテーブル"foo"に暗黙的なインデックス"foo_pkey"を作成します
    INSERT 0 3
    -bash-3.2$ psql -c "select * from foo;"
     k | v
    ---+----
     1 | 26
     2 | 34
     3 | 53
    (3 行)
    
    -bash-3.2$
    

    Slony-I のインストール、設定、起動

    CentOS 用の Slony-I パッケージを、yum でインストールします。PostgreSQL 8.4 用の最新の、yum 設定ファイルの RPM をインストールしてください。CentOS-5.5 で提供される postgres84-* とコンフリクトするファイルが多いので、Slony-I のインストールを終えたら、yum 設定ファイルはアンインストールしておきます。

    [root@node1 ~]# rpm \
     -Uvh http://www.pgrpms.org/reporpms/8.4/pgdg-centos-8.4-2.noarch.rpm
    http://www.pgrpms.org/reporpms/8.4/pgdg-centos-8.4-2.noarch.rpm を取得中
    準備中...                ########################################### [100%]
       1:pgdg-centos            ########################################### [100%]
    [root@node1 ~]# yum install -y slony1-II
    (中略)
    Installed:
      slony1-II.x86_64 0:2.0.3-2.rhel5
    
    Dependency Installed:
      perl-DBD-Pg.x86_64 0:1.49-2.el5_3.1        perl-DBI.x86_64 0:1.52-2.el5
    
    Complete!
    [root@node1 ~]# rpm -e pgdg-centos
    [root@node1 ~]#
    

    PL/pgSQL 言語サポートを導入し、Slony-I 用の DB ロールを作成します。

    [root@node1 ~]# su - postgres
    -bash-3.2$ createlang plpgsql
    -bash-3.2$ createuser --encrypted --pwprompt --superuser slony
    新しいロールのパスワード:<パスワード>
    もう一度入力してください:<パスワード>
    -bash-3.2$ psql -c \
     "update pg_authid set rolcatupdate = 't' where rolname = 'slony'"
    UPDATE 1
    -bash-3.2$
    

    slon デーモンがデータベースにアクセスできるようにします。

    [root@node1 ~]# su - postgres
    -bash-3.2$ cp /var/lib/pgsql/data/pg_hba.conf \
     /var/lib/pgsql/data/pg_hba.conf.orig2
    -bash-3.2$ vi /var/lib/pgsql/data/pg_hba.conf
    -bash-3.2$ diff -uNr /var/lib/pgsql/data/pg_hba.conf.orig2 \
     /var/lib/pgsql/data/pg_hba.conf
    --- /var/lib/pgsql/data/pg_hba.conf.orig2       2010-09-29 10:43:56.000000000 +0900
    +++ /var/lib/pgsql/data/pg_hba.conf     2010-09-29 10:45:38.000000000 +0900
    @@ -67,10 +67,13 @@
     # TYPE  DATABASE    USER        CIDR-ADDRESS          METHOD
    
     # "local" is for Unix domain socket connections only
    +local   postgres    slony                             md5
     local   all         all                               ident
     # IPv4 local connections:
    +host    postgres    slony       127.0.0.1/32          md5
     host    all         all         127.0.0.1/32          ident
     # IPv6 local connections:
     host    all         all         ::1/128               ident
     # IPv4 local network connections:
    +host    postgres    slony       192.168.1.0/24        md5
     host    postgres    postgres    192.168.1.0/24        md5
    -bash-3.2$ exit
    [root@node1 ~]# service postgresql restart
    postgresql サービスを停止中:                               [  OK  ]
    postgresql サービスを開始中:                               [  OK  ]
    [root@node1 ~]# cp /etc/slon.conf /etc/slon.conf.orig
    [root@node1 ~]# vi /etc/slon.conf
    [root@node1 ~]# diff -uNr /etc/slon.conf.orig /etc/slon.conf
    --- /etc/slon.conf.orig 2010-09-28 15:12:00.000000000 +0900
    +++ /etc/slon.conf      2010-09-28 15:15:04.000000000 +0900
    @@ -86,10 +86,10 @@
    
     # Set the cluster name that this instance of slon is running against
     # default is to read it off the command line
    -#cluster_name='sloncluster'
    +cluster_name='testcluster'
    
     # Set slon's connection info, default is to read it off the command line
    -#conn_info='host=/tmp port=5432 user=slony'
    +conn_info='host=localhost port=5432 dbname=postgres user=slony password=slony'
    
     # maximum time planned for grouped SYNCs
     # If replication is behind, slon will try to increase numbers of
    [root@node1 ~]# service slony1-II start
    slony1-II サービスを開始中:                                [  OK  ]
    [root@node2 ~]# chkconfig slony1-II on
    [root@node1 ~]#
    

    slon デーモンのログで、まずは slony ロールでの接続ができていることを確認します。まだクラスタの初期化をしていないので Slony-I としての動作ではエラーが出ていますが、問題ありません。

    [root@node1 ~]# tail /var/log/slony
    2010-09-29 10:49:18 JSTCONFIG main: String option lag_interval = [NULL]
    2010-09-29 10:49:18 JSTCONFIG main: String option command_on_logarchive = [NULL]
    2010-09-29 10:49:18 JSTCONFIG main: String option syslog_facility = LOCAL0
    2010-09-29 10:49:18 JSTCONFIG main: String option syslog_ident = slon
    2010-09-29 10:49:18 JSTCONFIG main: String option cleanup_interval = 10 minutes
    2010-09-29 10:49:18 JSTCONFIG slon: worker process created - pid = 4728
    2010-09-29 10:49:18 JSTERROR  cannot get sl_local_node_id -
     ERROR:  スキーマ"_testcluster"は存在しません
    LINE 1: select last_value::int4 from "_testcluster".sl_local_node_id
                                         ^
    2010-09-29 10:49:18 JSTFATAL  main: Node is not initialized properly - sleep 10s
    [root@node1 ~]#
    

    Slony-I クラスタの 1st ノードをセットアップ

    [root@node1 ~]# mv /etc/slon_tools.conf /etc/slon_tools.conf.orig
    [root@node1 ~]# vi /etc/slon_tools.conf
    [root@node1 ~]# cat /etc/slon_tools.conf
    $CLUSTER_NAME = 'testcluster';
    $LOGDIR = '/var/log/slony1';
    $DEBUGLEVEL = 2;
    
    add_node(
     node     => 1,
     host     => 'node1.priv',
     port     => 5432,
     dbname   => 'postgres',
     user     => 'slony',
     password => 'slony' );
    
    $MASTERNODE = 1;
    
    $SLONY_SETS = {
      "set1" => {
        "set_id" => 1,
        "table_id" => 1,
        "sequence_id" => 1,
        "pkeyedtables" => [
          'foo',
        ],
      },
    };
    
    1;
    [root@node1 ~]# slonik_init_cluster | slonik
    <stdin>:8: Set up replication nodes
    <stdin>:11: Next: configure paths for each node/origin
    <stdin>:12: Replication nodes prepared
    <stdin>:13: Please start a slon replication daemon for each node
    [root@node1 ~]# slonik_create_set set1 | slonik
    <stdin>:15: Subscription set 1 created
    <stdin>:16: Adding tables to the subscription set
    <stdin>:20: Add primary keyed table public.foo
    <stdin>:23: Adding sequences to the subscription set
    <stdin>:24: All tables added
    [root@node1 ~]#
    

    何か不具合が起きたら、”DROP SCHEMA _<クラスタ名> CASCADE” で、スキーマごとドロップしてからやり直してください。

    ログに、”configuration complete” が出ていれば、slon デーモンが正常に Slony-I の管理データにアクセスできています。

    [root@node1 ~]# less /var/log/slony
    ...
    2010-09-29 10:54:48 JSTCONFIG main: loading current cluster configuration
    2010-09-29 10:54:48 JSTCONFIG main: last local event sequence = 5000000001
    2010-09-29 10:54:48 JSTCONFIG main: configuration complete - starting threads
    2010-09-29 10:54:48 JSTINFO   localListenThread: thread starts
    2010-09-29 10:54:48 JSTCONFIG version for "host=localhost port=5432
     dbname=postgres user=slony password=slony" is 80404
    ...
    

    Slony-I スレーブノードの追加

    2 台目のサーバ上で、「PostgreSQL のインストール、設定、起動」と「Slony-I のインストール、設定、起動」を実行します。

    slon tools の設定ファイルに、2 つ目のノードを追加します。

    [root@node1 ~]# cp /etc/slon_tools.conf /etc/slon_tools.conf.orig2
    [root@node1 ~]# vi /etc/slon_tools.conf
    [root@node1 ~]# diff -uNr /etc/slon_tools.conf.orig2 /etc/slon_tools.conf
    --- /etc/slon_tools.conf.orig2  2010-09-28 17:24:20.000000000 +0900
    +++ /etc/slon_tools.conf        2010-09-28 17:23:36.000000000 +0900
    @@ -10,6 +10,14 @@
      user     => 'slony',
      password => 'slony' );
    
    +add_node(
    + node     => 2,
    + host     => 'node2.priv',
    + port     => 5432,
    + dbname   => 'postgres',
    + user     => 'slony',
    + password => 'slony' );
    +
     $MASTERNODE = 1;
    
     $SLONY_SETS = {
    [root@node1 ~]#
    

    対象スキーマをコピーしてからノードを Slony-I クラスタへ store し、購読設定をします。ここで注意としては、スキーマをコピーした後で、ノードを store することです。コピーの際に、node2 上にはまだ Slony-I 管理情報の名前空間が無いので、ここで node1 のテーブルに仕掛けてある Slony-I のトリガが落ちます (下記の通り、エラーになります)。その後の "store node" で名前空間が、"subscribe set" でトリガが追加されます。

    [root@node1 ~]# pg_dump -s -c "dbname=postgres user=slony password=slony" \
     -n public | psql "host=node2.priv dbname=postgres user=slony password=slony"
    SET
    SET
    SET
    SET
    SET
    SET
    SET
    ERROR:  リレーション"public.foo"は存在しません
    ERROR:  リレーション"public.foo"は存在しません
    ERROR:  リレーション"public.foo"は存在しません
    ERROR:  テーブル"foo"は存在しません
    DROP SCHEMA
    CREATE SCHEMA
    ALTER SCHEMA
    COMMENT
    SET
    SET
    SET
    CREATE TABLE
    ALTER TABLE
    ALTER TABLE
    ERROR:  スキーマ"_testcluster"は存在しません
    ERROR:  テーブル"foo"のトリガ"_testcluster_denyaccess"は存在しません
    ERROR:  スキーマ"_testcluster"は存在しません
    REVOKE
    REVOKE
    GRANT
    GRANT
    [root@node1 ~]# slonik_store_node 2 | slonik
    <stdin>:7: Set up replication nodes
    <stdin>:10: Next: configure paths for each node/origin
    <stdin>:13: Replication nodes prepared
    <stdin>:14: Please start a slon replication daemon for each node
    [root@node1 ~]# slonik_subscribe_set set1 2 | slonik
    <stdin>:4: NOTICE:  subscribe set: omit_copy=f
    <stdin>:4: NOTICE:  subscribe set: omit_copy=f
    CONTEXT:  SQL statement "SELECT  "_testcluster".subscribeSet_int( $1 ,  $2 ,  $3 ,  $4 ,  $5 )"
    PL/pgSQL 関数 "subscribeset" の 68 行目の型 PERFORM
    <stdin>:10: Subscribed nodes to set 1
    [root@node1 ~]#
    

    この時点で、node2 側の slon ログに "configuration complete" が出ていれば、追加ノードの slon は、正しく Slony-I データにアクセスできています。

    スイッチオーバー

    購読セットのマスターを、他のノードに移します。この際、旧マスターと新マスターの役割は入れ替わりますが、旧マスターとそれ以外のスレーブとの間のプロバイダ - レシーバの関係は変わりません。

    [root@node1 ~]# psql -U slony postgres -c "select * from _testcluster.sl_subscribe"
    ユーザ slony のパスワード:
     sub_set | sub_provider | sub_receiver | sub_forward | sub_active
    ---------+--------------+--------------+-------------+------------
           1 |            1 |            3 | t           | t
           1 |            1 |            2 | t           | t
    (2 行)
    
    [root@node1 ~]# slonik_move_set set1 1 2 | slonik
    <stdin>:5: Locking down set 1 on node 1
    <stdin>:9: Locked down - moving it
    <stdin>:11: Replication set 1 moved from node 1 to 2.  Remember to
    <stdin>:12: update your configuration file, if necessary, to note the new location
    <stdin>:13: for the set.
    [root@node1 ~]# psql -U slony postgres -c "select * from _testcluster.sl_subscribe"
    ユーザ slony のパスワード:
     sub_set | sub_provider | sub_receiver | sub_forward | sub_active
    ---------+--------------+--------------+-------------+------------
           1 |            1 |            3 | t           | t
           1 |            2 |            1 | t           | t
    (2 行)
    
    [root@node1 ~]#
    

    ですので、例えば { node1 → node2, node1 → node3, node1 → node4 } という構成でマスターを node1 から node2 に移したら、{ node2 → node1, node1 → node3, node1 → node4 } になります。

    スイッチバックすれば、元に戻ります。

    [root@node1 ~]# slonik_move_set set1 2 1 | slonik
    <stdin>:5: Locking down set 1 on node 2
    <stdin>:9: Locked down - moving it
    <stdin>:11: Replication set 1 moved from node 2 to 1.  Remember to
    <stdin>:12: update your configuration file, if necessary, to note the new location
    <stdin>:13: for the set.
    [root@node1 ~]# psql -U slony postgres -c "select * from _testcluster.sl_subscribe"
    ユーザ slony のパスワード:
     sub_set | sub_provider | sub_receiver | sub_forward | sub_active
    ---------+--------------+--------------+-------------+------------
           1 |            1 |            3 | t           | t
           1 |            1 |            2 | t           | t
    (2 行)
    
    [root@node1 ~]#
    

    node1 を、メンテナンスのために、Slony-I クラスタから取り除きたいと思います。では、node2 → node1 → node3 の時、node1 の unsubscribe をしたらどうなるのでしょうか?

    [root@node2 ~]# psql -U slony postgres -c "select * from _testcluster.sl_subscribe"
    ユーザ slony のパスワード:
     sub_set | sub_provider | sub_receiver | sub_forward | sub_active
    ---------+--------------+--------------+-------------+------------
           1 |            1 |            3 | t           | t
           1 |            2 |            1 | t           | t
    (2 行)
    
    [root@node2 ~]# slonik_unsubscribe_set set1 1 | slonik
    <stdin>:5: PGRES_FATAL_ERROR
     select "_testcluster".unsubscribeSet(1, 1);  - ERROR:  Slony-I: Cannot unsubscribe
     set 1 while being provider
    <stdin>:8: Failed to unsubscribe node 1 from set 1
    [root@node2 ~]#
    

    はい、できません。事前にプロバイダを変更し、node1 を、クラスタのリーフにしておく必要があります。再度、unsubscribe をせずに、プロバイダを変更して subscribe します。下記の作業は、新マスターである node2 で行なっています。

    [root@node2 ~]# cp /etc/slon_tools.conf /etc/slon_tools.conf.orig3
    [root@node2 ~]# vi /etc/slon_tools.conf
    [root@node2 ~]# diff -uNr /etc/slon_tools.conf.orig3 /etc/slon_tools.conf
    --- /etc/slon_tools.conf.orig3  2010-09-30 12:33:15.000000000 +0900
    +++ /etc/slon_tools.conf        2010-09-30 12:33:40.000000000 +0900
    @@ -26,7 +26,7 @@
      user     => 'slony',
      password => 'slony' );
    
    -$MASTERNODE = 1;
    +$MASTERNODE = 2;
    
     $SLONY_SETS = {
       "set1" => {
    [root@node2 ~]# slonik_subscribe_set set1 3 | slonik
    <stdin>:5: NOTICE:  subscribe set: omit_copy=f
    <stdin>:5: NOTICE:  subscribe set: omit_copy=f
    CONTEXT:  SQL statement
     "SELECT  "_testcluster".subscribeSet_int( $1 ,  $2 ,  $3 ,  $4 ,  $5 )"
    PL/pgSQL 関数 "subscribeset" の 68 行目の型 PERFORM
    <stdin>:11: Subscribed nodes to set 1
    [root@node2 ~]# psql -U slony postgres \
     -c "select * from _testcluster.sl_subscribe"
    ユーザ slony のパスワード:
     sub_set | sub_provider | sub_receiver | sub_forward | sub_active
    ---------+--------------+--------------+-------------+------------
           1 |            2 |            1 | t           | t
           1 |            2 |            3 | t           | t
    (2 行)
    
    [root@node2 bin]# slonik_unsubscribe_set set1 1 | slonik
    <stdin>:12: unsubscribed node 1 from set 1
    [root@node2 bin]# psql -U slony postgres -c \
     "select * from _testcluster.sl_subscribe"
    ユーザ slony のパスワード:
     sub_set | sub_provider | sub_receiver | sub_forward | sub_active
    ---------+--------------+--------------+-------------+------------
           1 |            2 |            3 | t           | t
    (1 行)
    
    [root@node2 bin]#
    

    slon_tools.conf を各ノードに配置し、それぞれの $MASTERNODE には、それぞれのノード名を指定しておいて、管理作業は現マスター上で行なう、とするのが良いかも知れません。

    参考:

    障害を起こしたスレーブのドロップ

    障害に見たてて、OS の終了処理をせずに、node3 の電源を切ります。手順的には、通常のドロップと同じです。

    [root@node1 ~]# slonik_drop_node 3 | slonik
    <stdin>:11: dropped node 3 cluster
    [root@node1 ~]#
    

    /etc/slon_tools.conf の設定も消しておきましょう。

    マスター障害に伴うフェイルオーバ

    障害に見たてて、OS の終了処理をせずに、node1 の電源を切ります。マスターを node2 に移すべく、node2 で作業をします。先に書いた通り、/etc/slon_tools.conf の $MASTERNODE は 2 です。

    [root@node2 ~]# slonik_failover 1 2 | slonik
    <stdin>:5: NOTICE:  failedNode: set 1 has other direct receivers - change providers only
    <stdin>:5: NOTICE:  failedNode: set 1 has other direct receivers - change providers only
    IMPORTANT: Last known SYNC for set 1 = 5000004784
    <stdin>:11: Replication sets originating on 1 failed over to 2
    [root@node2 ~]# psql -U slony postgres -c "select * from _testcluster.sl_node"
    ユーザ slony のパスワード:
     no_id | no_active |          no_comment
    -------+-----------+------------------------------
         1 | t         | Node 1 - postgres@node1.priv
         2 | t         | Node 2 - postgres@node2.priv
         3 | t         | Node 3 - postgres@node3.priv
    (3 行)
    
    [root@node2 ~]# psql -U slony postgres -c "select * from _testcluster.sl_subscribe"
    ユーザ slony のパスワード:
     sub_set | sub_provider | sub_receiver | sub_forward | sub_active
    ---------+--------------+--------------+-------------+------------
           1 |            2 |            3 | t           | t
    (1 行)
    
    [root@node2 ~]# slonik_drop_node 1 | slonik
    <stdin>:11: dropped node 1 cluster
    [root@node2 ~]# psql -U slony postgres -c "select * from _testcluster.sl_node"
    ユーザ slony のパスワード:
     no_id | no_active |          no_comment
    -------+-----------+------------------------------
         2 | t         | Node 2 - postgres@node2.priv
         3 | t         | Node 3 - postgres@node3.priv
    (2 行)
    
    [root@node2 ~]#
    

    slon_tools.conf の node1 の設定も消しておきましょう。

    後は、復旧→スレーブ追加を行ない、どうしても元に戻したければ、さらにスイッチオーバ→subscribe し直し、です。




    PostgreSQL-9.0 ホットスタンバイ構築

    PostgreSQL-9.0 で、プライマリサーバ 1 台にホットスタンバイサーバ 2 台を追加し、プライマリが死んだことを想定してホットスタンバイからプライマリへ昇格するところまでをやってみます。CentOS-5.5 を利用します。Twitter で @bose999 とやりあっていて、どうも自分の理解 (特に、WAL アーカイブと SR の関係) があいまいでしたので。

    以下の 3 台で構成します。非同期のレプリケーションでは、障害復旧後のスイッチバックは現実的ではないので、スイッチしたらそのまま運用します。そのため、”master”, “active”, “primary” や “slave”, “standby” といった、クラスタ内での役割を表す名前はつけない方が良いと思います。

    • node1.priv (192.168.1.24/24): 最初のプライマリ
    • node2.priv (192.168.1.27/24): ホットスタンバイ。node1 障害後のプライマリ
    • node3.priv (192.168.1.30/24): ホットスタンバイ

    インストール

    CentOS 用のパッケージを、yum でインストールします。

    上記サイトから、最新の、yum 設定ファイルの RPM をインストールしてください。

    [root@node1 ~]# rpm \
     -Uvh http://www.pgrpms.org/reporpms/9.0/pgdg-centos-9.0-2.noarch.rpm
    http://www.pgrpms.org/reporpms/9.0/pgdg-centos-9.0-2.noarch.rpm を取得中
    警告: /var/tmp/rpm-xfer.gb5Tw9: ヘッダ V3 DSA signature: NOKEY, key ID 442df0f8
    準備中...                ########################################### [100%]
       1:pgdg-centos            ########################################### [100%]
    [root@node1 ~]# yum install -y postgresql90 postgresql90-server
    (中略)
    Installed:
      postgresql90.x86_64 0:9.0.0-1PGDG.rhel5
      postgresql90-server.x86_64 0:9.0.0-1PGDG.rhel5
    
    Dependency Installed:
      postgresql90-libs.x86_64 0:9.0.0-1PGDG.rhel5
    
    Complete!
    [root@node1 ~]#
    

    postgresql90 のクライアントは、alternatives を使っているのでパスが通っています。postgresql90-server のコマンドへはパスが通っていませんので、パスを通すなりフルパスで指定するなりが必要です。

    一台目を、普通にセットアップ

    まずは、普通に (ストリーミングレプリケーションとホットスタンバイのことを考慮せずに) セットアップを行ないます。当座は、ホットスタンバイによる負荷分散が不要で、1 台で運用するという想定です。今回は、システムの init のサービススクリプトを用いずに、postgres ユーザが自前でサービスを用意します。それ以外は一般的な設定だと思いますので、サラッと流します。

    • ユーザ・グループ: postgres.postgres
    • DB クラスタのディレクトリ: /pgdata/
    • WAL アーカイブ先: /backup/walarch/
    • 週一での物理バックアップ先: /backup/pgdata/

    以下の設定では、レプリケーションへの流れがあるため、バックアップ (障害対策と PITR のため) については考慮していますが、性能チューニングやセキュリティについては考慮していません。

    [root@node1 ~]# mkdir -m 0700 -p /pgdata/ /backup/ \
     /backup/walarch/ /backup/pgdata/
    [root@node1 ~]# chown postgres.postgres /pgdata/ /backup/ \
     /backup/walarch/ /backup/pgdata/
    [root@node1 ~]# iptables -I INPUT -p tcp --dport 5432 -j ACCEPT
    [root@node1 ~]# service iptables save
    ファイアウォールのルールを /etc/sysconfig/iptables に保存中[  OK  ]
    [root@node1 ~]# su - postgres
    -bash-3.2$ /usr/pgsql-9.0/bin/initdb -D /pgdata/ --encoding=UTF-8 \
     --no-locale --username=admin --pwprompt --auth=md5
    データベースシステム内のファイルの所有者は"postgres"ユーザでした。
    このユーザがサーバプロセスを所有しなければなりません。
    
    データベースクラスタはロケールCで初期化されます。
    デフォルトのテキスト検索設定はenglishに設定されました。
    
    ディレクトリ/pgdataの権限を設定しています ... ok
    サブディレクトリを作成しています ... ok
    デフォルトのmax_connectionsを選択しています ... 100
    デフォルトの shared_buffers を選択しています ... 32MB
    設定ファイルを作成しています ... ok
    /pgdata/base/1にtemplate1データベースを作成しています ... ok
    pg_authidを初期化しています ... ok
    新しいスーパーユーザのパスワードを入力してください:<パスワード>
    再入力してください:<パスワード>
    パスワードを設定しています ... ok
    依存関係を初期化しています ... ok
    システムビューを作成しています ... ok
    システムオブジェクトの定義をロードしています ... ok
    変換を作成しています ... ok
    ディレクトリを作成しています ... ok
    組み込みオブジェクトに権限を設定しています ... ok
    情報スキーマを作成しています ... ok
    PL/pgSQL サーバサイド言語をロードしています ...ok
    template1データベースをバキュームしています ... ok
    template1からtemplate0へコピーしています ... ok
    template1からpostgresへコピーしています ... ok
    
    成功しました。以下を使用してデータベースサーバを起動することができます。
    
        /usr/pgsql-9.0/bin/postmaster -D /pgdata
    または
        /usr/pgsql-9.0/bin/pg_ctl -D /pgdata -l logfile start
    
    -bash-3.2$ cp /pgdata/postgresql.conf /pgdata/postgresql.conf.orig
    -bash-3.2$ vi /pgdata/postgresql.conf
    -bash-3.2$ diff -uNr /pgdata/postgresql.conf.orig /pgdata/postgresql.conf
    --- /pgdata/postgresql.conf.orig        2010-09-26 16:51:35.000000000 +0900
    +++ /pgdata/postgresql.conf     2010-09-26 16:52:56.000000000 +0900
    @@ -56,7 +56,7 @@
    
     # - Connection Settings -
    
    -#listen_addresses = 'localhost'                # what IP address(es) to listen on;
    +listen_addresses = '*'                 # what IP address(es) to listen on;
                                            # comma-separated list of addresses;
                                            # defaults to 'localhost', '*' = all
                                            # (change requires restart)
    @@ -150,7 +150,7 @@
    
     # - Settings -
    
    -#wal_level = minimal                   # minimal, archive, or hot_standby
    +wal_level = archive                    # minimal, archive, or hot_standby
     #fsync = on                            # turns forced synchronization on or off
     #synchronous_commit = on               # immediate fsync at commit
     #wal_sync_method = fsync               # the default is the first option
    @@ -177,10 +177,11 @@
    
     # - Archiving -
    
    -#archive_mode = off            # allows archiving to be done
    +archive_mode = on              # allows archiving to be done
                                    # (change requires restart)
    -#archive_command = ''          # command to use to archive a logfile segment
    -#archive_timeout = 0           # force a logfile segment switch after this
    +archive_command = 'rsync -a %p /backup/walarch/%f'
    +                               # command to use to archive a logfile segment
    +archive_timeout = 3600         # force a logfile segment switch after this
                                    # number of seconds; 0 disables
    
     # - Streaming Replication -
    -bash-3.2$ cp /pgdata/pg_hba.conf /pgdata/pg_hba.conf.orig
    -bash-3.2$ vi /pgdata/pg_hba.conf
    -bash-3.2$ diff -uNr /pgdata/pg_hba.conf.orig /pgdata/pg_hba.conf
    --- /pgdata/pg_hba.conf.orig    2010-09-26 13:51:48.000000000 +0900
    +++ /pgdata/pg_hba.conf 2010-09-26 13:52:30.000000000 +0900
    @@ -76,5 +76,6 @@
     local   all             all                                     md5
     # IPv4 local connections:
     host    all             all             127.0.0.1/32            md5
    +host    all             all             192.168.1.0/24          md5
     # IPv6 local connections:
     host    all             all             ::1/128                 md5
    -bash-3.2$ /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
    サーバは起動中です。
    -bash-3.2$ touch ~/.pgpass
    -bash-3.2$ chmod 0600 ~/.pgpass
    -bash-3.2$ echo "localhost:*:*:admin:admin" >> ~/.pgpass
    -bash-3.2$ createdb -U admin reptest
    -bash-3.2$ psql -U admin reptest
    psql (9.0.0)
    "help" でヘルプを表示します.
    
    reptest=# \q
    -bash-3.2$
    

    インスタンス起動を永続化しておきます。”crontab -e” を実行し、以下を追加します。”@reboot” は、最近めの cron でないとサポートしていないかも知れませんので、man で見ておいてください。

    @reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
    

    これでは WAL のアーカイブがたまり続けますので、10 日 (240 時間) より古いものは消して行くように設定します。”crontab -e” を実行し、以下を追加します。

    0 5 * * * /usr/bin/tmpwatch 240 /backup/walarch/
    

    定期的な物理バックアップ用のスクリプトを作成します。

    -bash-3.2$ touch /pgdata/backup-pgdata
    -bash-3.2$ chmod +x /pgdata/backup-pgdata
    -bash-3.2$ vi /pgdata/backup-pgdata
    -bash-3.2$ cat /pgdata/backup-pgdata
    #!/bin/sh
    rm -fr /backup/pgdata.new/
    psql -U admin -c "select pg_start_backup('$(date +%Y%m%d%H%M)')" template1 &&
     sleep 5 && \
     rsync -ar --link-dest=/backup/pgdata/ /pgdata/ /backup/pgdata.new/ &&
     mv /backup/pgdata /backup/pgdata.old && \
     mv /backup/pgdata.new/ /backup/pgdata/ && rm -fr /backup/pgdata.old
    psql -U admin -c "select pg_stop_backup()" template1
    -bash-3.2$ /pgdata/backup-pgdata
     pg_start_backup
    -----------------
     0/7000020
    (1 行)
    
    NOTICE:  pg_stop_backup complete, all required WAL segments have been archived
     pg_stop_backup
    ----------------
     0/70000A0
    (1 行)
    
    -bash-3.2$
    

    バックアップを、週に一回定期実行します。”crontab -e” を実行し、以下を追加します。

    0 5 * * 0 /pgdata/backup-pgdata
    

    crontab は、全体で以下のようになっているかと思います。

    -bash-3.2$ crontab -l
    @reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
    0 5 * * * /usr/bin/tmpwatch 240 /backup/walarch/
    0 5 * * 0 /pgdata/backup-pgdata
    -bash-3.2$
    

    以上の設定で、キャパシティが一杯になるまでは 1 台で運用していた、という設定で次へ行きます。

    1 台目のサーバをプライマリ化

    キャパシティが厳しくなってきたので、ホットスタンバイ サーバを追加するとことにします。

    まずは、後ほどホットスタンバイ側から DB アカウントでパスワードなしの SSH ログインする必要がでてきますので、もしまだ存在しないようでしたら、RSA キー登録のために必要なパスワードとキーチェーン ファイルを用意しておきます。

    [root@node1 ~]# passwd postgres
    Changing password for user postgres.
    New UNIX password:<パスワード>
    Retype new UNIX password:<パスワード>
    passwd: all authentication tokens updated successfully.
    [root@node1 ~]# su - postgres
    -bash-3.2$ mkdir ~/.ssh/
    -bash-3.2$ chmod 0700 ~/.ssh/
    -bash-3.2$ touch ~/.ssh/authorized_keys
    -bash-3.2$ chmod 0600 ~/.ssh/authorized_keys
    -bash-3.2$
    

    ストリーミングレプリケーションのプライマリとして働くように設定を変更し、設定を反映させるために再起動します。

    -bash-3.2$ cp /pgdata/postgresql.conf /pgdata/postgresql.conf.orig2
    -bash-3.2$ vi /pgdata/postgresql.conf
    -bash-3.2$ diff -uNr /pgdata/postgresql.conf.orig2 /pgdata/postgresql.conf
    --- /pgdata/postgresql.conf.orig2       2010-09-26 14:39:18.000000000 +0900
    +++ /pgdata/postgresql.conf     2010-09-26 14:42:54.000000000 +0900
    @@ -150,7 +150,7 @@
    
     # - Settings -
    
    -wal_level = archive                    # minimal, archive, or hot_standby
    +wal_level = hot_standby                        # minimal, archive, or hot_standby
     #fsync = on                            # turns forced synchronization on or off
     #synchronous_commit = on               # immediate fsync at commit
     #wal_sync_method = fsync               # the default is the first option
    @@ -186,14 +186,14 @@
    
     # - Streaming Replication -
    
    -#max_wal_senders = 0           # max number of walsender processes
    +max_wal_senders = 10           # max number of walsender processes
     #wal_sender_delay = 200ms      # walsender cycle time, 1-10000 milliseconds
     #wal_keep_segments = 0         # in logfile segments, 16MB each; 0 disables
     #vacuum_defer_cleanup_age = 0  # number of xacts by which cleanup is delayed
    
     # - Standby Servers -
    
    -#hot_standby = off                     # "on" allows queries during recovery
    +hot_standby = on                       # "on" allows queries during recovery
     #max_standby_archive_delay = 30s       # max delay before canceling queries
                                            # when reading WAL from archive;
                                            # -1 allows indefinite delay
    -bash-3.2$ cp /pgdata/pg_hba.conf /pgdata/pg_hba.conf.orig2
    -bash-3.2$ vi /pgdata/pg_hba.conf
    -bash-3.2$ diff -uNr /pgdata/pg_hba.conf.orig2 /pgdata/pg_hba.conf
    --- /pgdata/pg_hba.conf.orig2   2010-09-26 14:44:01.000000000 +0900
    +++ /pgdata/pg_hba.conf 2010-09-26 14:45:02.000000000 +0900
    @@ -79,3 +79,5 @@
     host    all             all             192.168.1.0/24          md5
     # IPv6 local connections:
     host    all             all             ::1/128                 md5
    +
    +host    replication     admin           192.168.1.0/24          md5
    -bash-3.2$ /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ restart
    サーバ停止処理の完了を待っています....完了
    サーバは停止しました
    サーバは起動中です。
    -bash-3.2$
    

    ホットスタンバイ追加のたびに作成するのは面倒なので、ホットスタンバイ サーバ共通の recovery.conf を事前に作っておきます。

    -bash-3.2$ cp /usr/pgsql-9.0/share/recovery.conf.sample \
     /pgdata/recovery.conf.hotstby
    -bash-3.2$ vi /pgdata/recovery.conf.hotstby
    -bash-3.2$ diff -uNr \
     /usr/pgsql-9.0/share/recovery.conf.sample /pgdata/recovery.conf.hotstby
    --- /usr/pgsql-9.0/share/recovery.conf.sample   2010-09-17 23:12:06.000000000 +0900
    +++ /pgdata/recovery.conf.hotstby       2010-09-26 17:18:13.000000000 +0900
    @@ -43,7 +43,7 @@
     # NOTE that the basename of %p will be different from %f; do not
     # expect them to be interchangeable.
     #
    -#restore_command = ''          # e.g. 'cp /mnt/server/archivedir/%f %p'
    +restore_command = 'cp /backup/walarch/%f %p'
     #
     #
     # archive_cleanup_command
    @@ -95,9 +95,9 @@
     # connection settings primary_conninfo, and receives XLOG records
     # continuously.
     #
    -#standby_mode = 'off'
    -#
    -#primary_conninfo = ''         # e.g. 'host=localhost port=5432'
    +standby_mode = 'on'
    +
    +primary_conninfo = 'host=node1.priv port=5432 user=admin password=admin'
     #
     #
     # By default, a standby server keeps streaming XLOG records from the
    @@ -106,7 +106,7 @@
     # Server will poll the trigger file path periodically and stop streaming
     # when it's found.
     #
    -#trigger_file = ''
    +trigger_file = '/var/lib/pgsql/trigger'
     #
     #---------------------------------------------------------------------------
     # HOT STANDBY PARAMETERS
    -bash-3.2$
    

    “wal_mode = archive” で作成された WAL を一掃してから、一度バックアップを取得します。これが、スタンバイ サーバのベースとなります。

    -bash-3.2$ rm -f /backup/walarch/*
    -bash-3.2$ /pgdata/backup-pgdata
     pg_start_backup
    -----------------
     0/A000020
    (1 行)
    
    NOTICE:  pg_stop_backup complete, all required WAL segments have been archived
     pg_stop_backup
    ----------------
     0/A0000A0
    (1 行)
    
    -bash-3.2$
    

    ホットスタンバイを追加

    プライマリの「インストール」と同様にして、”postgresql90″ と “postgresql90-server” をインストールします。

    [root@node2 ~]# rpm \
     -Uvh http://www.pgrpms.org/reporpms/9.0/pgdg-centos-9.0-2.noarch.rpm
    [root@node2 ~]# yum install -y postgresql90-server
    (中略)
    
    Installed:
      postgresql90-server.x86_64 0:9.0.0-1PGDG.rhel5
    
    Dependency Installed:
      postgresql90.x86_64 0:9.0.0-1PGDG.rhel5
      postgresql90-libs.x86_64 0:9.0.0-1PGDG.rhel5
    
    Complete!
    [root@node2 ~]#
    

    ディレクトリを用意し、ファイアウォールにポートをあけます。

    [root@node2 ~]# mkdir -m 0700 -p /pgdata/ /backup/ \
     /backup/walarch/ /backup/pgdata/
    [root@node2 ~]# chown postgres.postgres /pgdata/ /backup/ \
     /backup/walarch/ /backup/pgdata/
    [root@node2 ~]# iptables -I INPUT -p tcp --dport 5432 -j ACCEPT
    [root@node2 ~]# service iptables save
    ファイアウォールのルールを /etc/sysconfig/iptables に保存中[  OK  ]
    [root@node2 ~]#
    

    SSH で、パスワードなしでプライマリにログインできるようにします。

    [root@node2 ~]# su - postgres
    -bash-3.2$ ssh-keygen
    Generating public/private rsa key pair.
    Enter file in which to save the key (/var/lib/pgsql/.ssh/id_rsa): <Enter>
    Created directory '/var/lib/pgsql/.ssh'.
    Enter passphrase (empty for no passphrase): <Enter>
    Enter same passphrase again: <Enter>
    Your identification has been saved in /var/lib/pgsql/.ssh/id_rsa.
    Your public key has been saved in /var/lib/pgsql/.ssh/id_rsa.pub.
    The key fingerprint is:
    a6:bd:ea:76:8b:93:de:3f:7e:48:16:07:c3:a1:f5:ce postgres@node2.priv
    -bash-3.2$ cat ~/.ssh/id_rsa.pub |
     ssh node1.priv "cat >> .ssh/authorized_keys"
    The authenticity of host 'node1.priv (192.168.1.24)' can't be established.
    RSA key fingerprint is 4c:98:4e:3c:dc:d0:e9:49:d0:d0:02:59:66:f8:56:59.
    Are you sure you want to continue connecting (yes/no)? yes
    Warning: Permanently added 'node1.priv,192.168.1.24' (RSA) to the list of
     known hosts.
    postgres@node1.priv's password: <パスワード>
    -bash-3.2$ ssh node1.priv
    Last login: Sun Sep 26 15:21:11 2010 from queen-centos2.priv
    -bash-3.2$ hostname
    node1.priv
    -bash-3.2$ exit
    logout
    Connection to node1.priv closed.
    -bash-3.2$
    

    ベースバックアップと WAL アーカイブを、プライマリから複製します。今回はサーバ間で、rsync による定期コピーで共有を行なっていますが、既存のバックアップサーバ等が存在するのであれば、NFS 等を用いた方がスマートだと思います。

    -bash-3.2$ rsync -azr --delete --rsh=ssh node1.priv:/backup/pgdata/ /pgdata/
    -bash-3.2$ rsync -azr --delete --rsh=ssh node1.priv:/backup/walarch/ /backup/walarch/
    -bash-3.2$
    

    1 時間ごとに WAL アーカイブをプライマリから複製するよう、”crontab -e” で以下を追加しておきます。

    0 * * * * rsync -azr --delete --rsh=ssh node1.priv:/backup/walarch/ /backup/walarch/
    

    recovery.conf を用意し、ホットスタンバイ サーバを起動します。ついでに .pgpass を用意しておきます。

    ここで、postgresql.conf の archive_command はプライマリのそれ、そのものなので、フェイルオーバ後は、/backup/walarch/ にアーカイブをします。今回は障害時に自動での切り替えは行ないませんので問題ないとは思いますが、自動での切り替えで、しかも NFS での共有だと、両ノードがスプリットブレインで同じディレクトリに書きこむ怖れがあります。下記サイトに、そのあたりの考察があって参考になりました:

    -bash-3.2$ cp /pgdata/recovery.conf.hotstby /pgdata/recovery.conf
    -bash-3.2$ rm -f /pgdata/postmaster.pid
    -bash-3.2$ /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
    サーバは起動中です。
    -bash-3.2$ touch ~/.pgpass
    -bash-3.2$ chmod 0600 ~/.pgpass
    -bash-3.2$ echo "localhost:*:*:admin:admin" >> ~/.pgpass
    -bash-3.2$
    

    起動を永続化します。

    @reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
    

    crontab は、最終的に以下のようになっていると思います。

    -bash-3.2$ crontab -l
    0 * * * * rsync -azr --delete --rsh=ssh node1.priv:/backup/walarch/ /backup/walarch/
    @reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
    -bash-3.2$
    

    2 つ目、3 つ目のホットスタンバイも、同様の手順で増やします。スレーブが障害を起こして再度追加する場合も、手順は同じです。

    スレーブからマスターへの昇格

    ホットスタンバイが障害を起こしたとしても、前項のように簡単にリプレースができるのですが、プライマリが障害を起こすと面倒です。プライマリだけでも、DRBD なり共有ディスクなりで、同期で HA 化しておくと良いかも知れません。

    障害に見立てて、ブチ切りで、プライマリ (node1) のプロセスを落とします。

    [root@node1 ~]# su - postgres
    -bash-3.2$ /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ -m immediate stop
    サーバ停止処理の完了を待っています...完了
    サーバは停止しました
    -bash-3.2$
    

    書きこみができないという報告なり、プロセス監視なりでこの事態を把握してください。修復不能と判断したら、node1 を捨てます。cron のエントリを削除します。

    -bash-3.2$ crontab -l
    @reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
    0 5 * * * /usr/bin/tmpwatch 240 /backup/walarch/
    0 5 * * 0 /pgdata/backup-pgdata
    -bash-3.2$ crontab -e
    -bash-3.2$ crontab -l
    -bash-3.2$
    

    新しくプライマリになるサーバ以外のホットスタンバイ サーバも捨てます。プロセスを停止し、crontab のエントリを消します。

    [root@node3 ~]# su - postgres
    -bash-3.2$ /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ stop
    サーバ停止処理の完了を待っています....完了
    サーバは停止しました
    -bash-3.2$ crontab -l
    0 * * * * rsync -azr --delete --rsh=ssh node1.priv:/backup/walarch/ /backup/walarch/
    @reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
    -bash-3.2$ crontab -e
    -bash-3.2$ crontab -l
    -bash-3.2$
    

    新しくプライマリとなる node2 上でトリガを発行して、node2 をプライマリにします。crontab で、node1 から WAL アーカイブを同期していたエントリを消し、かわりに、古い WAL アーカイブを削除するためのエントリと、物理バックアップのエントリを追加します。

    [root@node2 ~]# su - postgres
    -bash-3.2$ touch /var/lib/pgsql/trigger
    -bash-3.2$ crontab -l
    0 * * * * rsync -azr --delete --rsh=ssh node1.priv:/backup/walarch/ /backup/walarch/
    @reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
    -bash-3.2$ crontab -e
    -bash-3.2$ crontab -l
    @reboot /usr/pgsql-9.0/bin/pg_ctl -D /pgdata/ start
    0 5 * * * /usr/bin/tmpwatch 240 /backup/walarch/
    0 5 * * 0 /pgdata/backup-pgdata
    -bash-3.2$
    

    「1 台目のサーバをプライマリ化」と同等のやり残しをしておきます。

    [root@node1 ~]# passwd postgres
    Changing password for user postgres.
    New UNIX password:<パスワード>
    Retype new UNIX password:<パスワード>
    passwd: all authentication tokens updated successfully.
    [root@node1 ~]# su - postgres
    -bash-3.2$ mkdir -p ~/.ssh/
    -bash-3.2$ chmod 0700 ~/.ssh/
    -bash-3.2$ touch ~/.ssh/authorized_keys
    -bash-3.2$ chmod 0600 ~/.ssh/authorized_keys
    -bash-3.2$ rm -f /backup/walarch/*
    -bash-3.2$ cp /pgdata/recovery.conf.hotstby /pgdata/recovery.conf.hotstby.orig
    -bash-3.2$ vi /pgdata/recovery.conf.hotstby
    -bash-3.2$ diff -uNr /pgdata/recovery.conf.hotstby.orig \
     /pgdata/recovery.conf.hotstby
    --- /pgdata/recovery.conf.hotstby.orig  2010-09-26 21:48:16.000000000 +0900
    +++ /pgdata/recovery.conf.hotstby       2010-09-26 21:48:34.000000000 +0900
    @@ -97,7 +97,7 @@
     #
     standby_mode = 'on'
    
    -primary_conninfo = 'host=node1.priv port=5432 user=admin password=admin'
    +primary_conninfo = 'host=node2.priv port=5432 user=admin password=admin'
     #
     #
     # By default, a standby server keeps streaming XLOG records from the
    -bash-3.2$ /pgdata/backup-pgdata
     pg_start_backup
    -----------------
     0/A000020
    (1 行)
    
    NOTICE:  pg_stop_backup complete, all required WAL segments have been archived
     pg_stop_backup
    ----------------
     0/A0000A0
    (1 行)
    
    -bash-3.2$
    

    あとは、「ホットスタンバイを追加」の項の、”node1″ を “node2″ に読み替えて、ホットスタンバイを追加していきます。




    rsync(1) で atomic 同期

    とあるデータ領域ディレクトリのバックアップを、ごく普通に rsync(1) コマンドで、既存のバックアップディレクトリとの差分で取っていて、ふと不安になりました。課題は以下:

    • バックアップ中にコケたら、整合性のないツリーだけが残ってしまう (元のデータすら残らない)
    • かといって、毎度新しいツリーにバックアップをしてからリネームする方法では、やたらと時間がかかる
    • できれば、複数世代のバックアップを残したい
    • できれば、複数ディレクトリを整合状態で複製したい

    そこで rsync(1) の man を見ていると、以下のようなオプションがありました:

    --link-dest=DIR        hardlink to files in DIR when unchanged
    

    これを使えば、前回のコピーからの増分だけをコピーして、変更がなかったファイルについてはハードリンクを張った複製のテンポラリを作れます。コピーが終了したら、テンポラリから最終的な名前にすれば OK:

    $ rsync -avzr --rsh=ssh –link-dest=/backup/20100922/ /data/ \
     knaka@backup.priv:/backup/20100923.tmp/
    $ ssh knaka@backup.priv "mv /backup/20100923.tmp/ /backup/20100923/"
    

    これで行きたいと思います。

    注意:

    言うまでもありませんが、ここでいう「アトミック」は「rsync が成功するか否か」の意味での「アトミック」でしかありません。ファイルシステムでトランザクションやスナップショットの機能を用いているわけではありませんので、取得できるのは、決して rsync 開始や終了の瞬間のスナップショットでもなければ、ディレクトリ内のファイル間の整合性を保証するものでもありません。rsync 中に書き換えがおきれば、タイミングによって、その変更は destination 側に対して反映されるかも知れませんし、されないかも知れません。排他ロックがかかっているファイルは、読むこともできません。

    ていうか、そんなことができたらデータベースのバックアップとかに苦労しないよね。

    スクリプト rsync-dirs-atomic(1)

    以下を行なうスクリプトを書いてみました:

    • 複数ディレクトリを、アトミックに同期する (コピー中は “.tmp” の postfix がつく)
    • 最新の版に “latest” のシンボリックリンクを張る

    ついでに:

    • コピー元のシンボリックリンクを辿る
    • ローカルへのコピーもできる
    • “-d” オプションで、一定日数より古いバックアップの削除をする
    • エラーチェックもする

    これで行けてると思いますが、問題や間違いがありそうでしたら指摘していただけると助かります。

    #!/bin/sh
    # -*- coding: utf-8 -*-
    
    function usage {
      echo "Usage: $0" \
       "[OPTIONS] SOURCE_DIR [SOURCE_DIRs] [[USER@]HOSTNAME:]/DEST_DIR/" 1>&2
    }
    
    days=
    while getopts d: arg
    do
      case $arg in
        d)
          days=$OPTARG
          ;;
        ?)
          usage
          exit 1
          ;;
      esac
    done
    shift $(expr $OPTIND - 1)
    if ! test -n "$1" -a -n "$2"
    then
      usage
      exit 1
    fi
    sdirs=
    while test -n "$2"
    do
      sdirs="$sdirs $1"
      shift
    done
    userhostddirbase=$1
    # --------------------------------------------------------------------
    # Source side checks
    for sdir in $sdirs
    do
      if test $(echo $sdir | sed -e 's/^\(.\).*/\1/') != "/"
      then
        echo \"$sdir\" is not an absolute local path. 1>&2
        exit 1
      fi
      if test $(echo $sdir | sed -e 's/.*\(.\)$/\1/') != "/"
      then
        echo \"$sdir\" does not end with \"/\". 1>&2
        exit 1
      fi
      if ! test -d $sdir
      then
        echo Directory \"$sdir\" does not exit. 1>&2
      fi
      if ! test -r $sdir
      then
        echo Cannot read $sdir. 1>&2
        exit 1
      fi
    done
    # --------------------------------------------------------------------
    # Destination side checks
    userhost=$(echo $userhostddirbase | cut -s -d : -f 1)
    ddirbase=$(echo $userhostddirbase | cut -s -d : -f 2)
    if test -n "$userhost" -a -n "$ddirbase"
    then
      destshell="ssh $userhost"
    else
      ddirbase=$userhostddirbase
      destshell="sh -c"
    fi
    if test -n "$userhost" && ! ssh -o PasswordAuthentication=no \
     -o StrictHostKeyChecking=yes $userhost "/bin/true"
    then
      exit 1
    fi
    if ! $destshell "test -d $ddirbase/"
    then
      echo Directory \"$userhost:$ddirbase/\" does not exist. 1>&2
      exit 1
    fi
    if ! $destshell "test -w $ddirbase/"
    then
      echo Cannot write to $userhost:$ddirbase/. 1>&2
      exit 1
    fi
    # --------------------------------------------------------------------
    # Copy them
    stamp=$(date +%Y-%m-%d-%H-%M-%S)
    newdir=$ddirbase/$stamp
    newdirtmp=$newdir.tmp
    $destshell "rm -fr $newdir $newdirtmp; mkdir -p $newdirtmp"
    latestdir=$ddirbase/latest
    for sdir in $sdirs
    do
      for dir in $(echo $sdir; find $sdir -type l |
       while read i; do readlink $i; done )
      do
        id=$(echo $dir | sed -e 's@/@_@g')
        linkdest=""
        if $destshell "test -d $latestdir/$id/"
        then
          linkdest="--link-dest=$latestdir/$id/"
        fi
        if test -n "$userhost"
        then
          rsync -avzr --rsh=ssh $linkdest $dir $userhost:$newdirtmp/$id/
        else
          rsync -avzr $linkdest $dir $newdirtmp/$id/
        fi
      done
    done
    # Link "latest" and remove old one.
    $destshell "mv $newdirtmp $newdir && rm -f $latestdir &&
     ln -sf $(basename $newdir) $latestdir"
    if test -n "$days"
    then
      $destshell "find $ddirbase -maxdepth 1 -type d -mtime +$days |
       xargs rm -fr"
    fi
    



    ソース斜め読み: pldebugger

    CentOS で PL/pgSQL デバッガ – Ayutaya.com」がうまく動かずに、パッチを作るのに結構手間がかかったので、ソースを斜め読みした結果を残しておきます。とりあえず、グローバル・ブレークポイントでのデバッグについてだけ書きます。ローカル・ブレークポイントについては、気が向いたら書きます。

    PL/pgSQL デバッガ (グローバル・ブレークポイント)

    # 手描き…

    上記の例では、pgAdmin-III から関数にブレークポイントを仕掛け、psql から関数を呼び出し、pgAdmin-III でデバッグを行なう、という流れで行きます。セッション間で AF_INET での IPC 通信をするところが分かれば理解できると思います。

    まず、pgAdmin-III から PostgreSQL へ、普通に接続します。普通にセッションプロセスが fork(2) します (図の、「通常」セッションプロセス)。

    関数にブレークポイントを設定するよう指示すると、pgAdmin-III は、上記の認証情報を用いて、もう一本、デバッガ用の接続を確立します (図の、「デバッガ」セッションプロセス)。PostgreSQL 拡張モジュール pldbgapi.so を用いて、デバッガセッションとして働きます。

    このデバッグ接続ですが、特権ユーザでしか接続できません。実際に何をしているのか (どんな SQL クエリを送っているのか) を見たくて log_min_duration_statement を 0 に設定してみたのですが、pgAdmin-III が「気を効かせて」、特権ユーザなのを良いことに、ログ出力を抑制してくれます。pgAdmin-III をリビルドして、何をしているのかをログに出すには、pgAdmin-III のソースの以下の部分を削除します:

    perl -p -i -e 's/.*SET log_min_messages TO fatal.*//' \
     ./pgadmin/debugger/ctlCodeWindow.cpp \
     ./pgadmin/debugger/dlgDirectDbg.cpp
    

    そういえば、g++-4.1.2 + wxGTK-devel で、配列添え字オペレータの演算子オーバーロードで、引数の int と size_t が曖昧でビルドできない、と言われるかも知れません。ヘッダでどちらかをコメントアウトすれば通ります。

    その結果出てくるのが、以下のようなログです (適宜行番号 & 改行追加):

    1: LOG:  期間: 1.226 ミリ秒  文: SELECT count(*) AS count, proname
        FROM pg_proc WHERE oid = 16446 GROUP BY proname
    2: LOG:  期間: 0.414 ミリ秒  文: set client_encoding to 'UNICODE'
    3: LOG:  期間: 1.437 ミリ秒  文: SELECT * from pldbg_create_listener()
    4: LOG:  期間: 1.398 ミリ秒  文: SELECT *, 'NULL' as pid
        FROM pldbg_get_target_info('16446', 'o')
    5: LOG:  期間: 0.439 ミリ秒  文: SELECT version();
    6: LOG:  期間: 0.610 ミリ秒  文: SELECT *
        FROM pldbg_set_global_breakpoint(1, 16446, NULL, NULL)
    

    pg_proc テーブルで、ターゲット関数を確認。pldbg_create_listener() で、デバッガセッションは 127.0.0.1 上に AF_INET でデバッグサーバのポートを開きます。このポートは、実際にデバッグモードに入った後で、デバッガとターゲットが独自のプロトコルで通信を行なうために用いられます。pldbg_get_target_info() でターゲットを指定し、デバッグセッションは、共有メモリ上にブレークポイントとポート番号の情報を書き込みます。最後に、pldbg_set_global_breakpoint() で、デバッグセッションはブロックし、待ちに入ります。

    pldbgapi.c:

    Datum pldbg_create_listener( PG_FUNCTION_ARGS )
    {
      debugSession * session =
       MemoryContextAlloc( TopMemoryContext, sizeof( *session ));
      initializeModule();
      // これ
      session->listener = allocateServerListener( &session->serverPort );
      session->serverSocket = -1;
      mostRecentSession = session;
      PG_RETURN_INT32( addSession( session ));
    }
    

    次に、psql(1) からサーバに接続し、新しいセッションプロセスを fork(2) します (図の、「ターゲット」セッションプロセス)。ここで働く plugin_debugger.so は、いわゆる普通の PostgreSQL モジュール (SQL から呼び出す) とは違って、PL/pgSQL の拡張モジュールです。ストアドプロシージャの各行の実行をフックすることができるので、これを利用してブレークポイントやステップ実行を実現します。

    plugin_debugger.c:

    // 関数の頭と、行の頭でフックがかかるようにする
    static PLpgSQL_plugin plugin_funcs =
     { dbg_startup, NULL, NULL, dbg_newstmt, NULL };
    
    void _PG_init( void )
    {
      PLpgSQL_plugin ** var_ptr =
       (PLpgSQL_plugin **) find_rendezvous_variable( plugin_name );
      reserveBreakpoints();
      *var_ptr = &plugin_funcs;
    }
    

    plpgsql.h:

    typedef struct
    {
      /* Function pointers set up by the plugin */
      void (*func_setup) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
      void (*func_beg) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
      void (*func_end) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
      void (*stmt_beg) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt);
      void (*stmt_end) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt);
    
      /* Function pointers set by PL/pgSQL itself */
      void (*error_callback) (void *arg);
      void (*assign_expr) (PLpgSQL_execstate *estate, PLpgSQL_datum *target,
       PLpgSQL_expr *expr );
    } PLpgSQL_plugin;
    

    dbg_startup() でブレークポイントを確認して PLpgSQL_execstate * estate に関数ごとにプラグインの固有情報を書きこみます。

    plugin_debugger.c:

    static void dbg_startup( PLpgSQL_execstate * estate, PLpgSQL_function * func )
    {
      if( !breakpointsForFunction( funcGetOid( func )) &&
       !per_session_ctx.step_into_next_func )
      {
        estate->plugin_info = NULL;
        return;
      }
      // estate->plugin_info 構造体が、dbg_newstmt() に渡ります
      initialize_plugin_info(estate, func);
    }
    

    その固有情報にもとづいて、dbg_newstmt() が行レベルデバッグを実現します。下記、attach_to_proxy() でデバッガセッションとの接続が確立されます (何かファイアウォールでこの接続が阻害されたりしてた? 確信無い)。

    static void dbg_newstmt( PLpgSQL_execstate * estate, PLpgSQL_stmt * stmt )
    {
      // ブレークポイントが無ければ即リターン
      if( frame->plugin_info == NULL )
        return;
      else
      {
        Breakpoint * breakpoint = NULL;
        // 該当行であればデバッグモードに入ります
        if(( dbg_info->stepping ) ||
         breakAtThisLine(
          &breakpoint,
          &breakpointScope,
          funcGetOid( dbg_info->func ),
          isFirstStmt( stmt, dbg_info->func ) ? -1 : stmt->lineno ))
        {
          dbg_info->stepping = TRUE;
        }
        else
          return;
        attach_to_proxy( breakpoint );
        // 以下、デバッガセッションとターゲットセッションのやりとり開始
      }
      return;
    }
    



    CentOS で PL/pgSQL デバッガ

    PostgreSQL 用の GUI インタフェースである pgAdmin-III (pgAdmin: PostgreSQL administration and management tools) ですが、出来が良いわりにはあまり用いられていないようです。コマンドラインのツールでだいたい足りてしまうからでしょうが、ストアドプロシージャのデバッグの際などには、変数の状態等を見ながら作業ができる GUI もそれなりにありがたいですので、pgAdmin-III で動作する PL/pgSQL のデバッガ (PgFoundry: PLpgSQL Debugger by EnterpriseDB: Project Info) を CentOS-5.5 上で試してみたいと思います。

    pgAdmin-III でのデバッグ

    Red Hat Enterprise Linux (RHEL) なり CentOS の ver. 5 系は 2005 年のデビューですので、ずっと PostgreSQL のバージョンが 8.1 と、いくらなんでも古かったのですが、RHEL-5.5 からは PostgreSQL-8.4 が使えるようになりました。ここでは、そちらで行ってみたいと思います。

    サーバの設定

    パッケージ “postgresql-server” と “postgresql84-server” は排他です。何かの依存で “postgresql-server” が先に入ってしまうと、後から “postgresql84-server” を入れられませんので、注意してください。

    [root@queen-centos ~]# yum install -y postgresql84 postgresql84-server
    (中略)
    [root@queen-centos ~]#
    

    データベースを初期化、起動し、システム起動時に起動するようにします。

    [root@queen-centos ~]# service postgresql initdb
    データベースを初期化中:                                    [  OK  ]
    [root@queen-centos ~]# service postgresql start
    postgresql サービスを開始中:                               [  OK  ]
    [root@queen-centos ~]# chkconfig postgresql on
    [root@queen-centos ~]#
    

    CentOS のデフォルトだと、PostgreSQL は ident 認証になっていますので、”postgres” ユーザに su して、テスト用のデータベースと DB ロールを作成します。ついでに、リモートからアクセスできるようにしておきます。pg_hba.conf のアクセス制御のネットワークの指定については、環境ごとに適宜読み替えてください。

    [root@queen-centos ~]# su - postgres
    -bash-3.2$ createdb pgtest
    -bash-3.2$ createuser --encrypted --pwprompt \
     --superuser --createrole --createdb pgtest
    新しいロールのパスワード:
    もう一度入力してください:
    -bash-3.2$ cp /var/lib/pgsql/data/postgresql.conf \
     /var/lib/pgsql/data/postgresql.conf.orig
    -bash-3.2$ vi /var/lib/pgsql/data/postgresql.conf
    -bash-3.2$ diff -uNr /var/lib/pgsql/data/postgresql.conf.orig \
     /var/lib/pgsql/data/postgresql.conf
    --- /var/lib/pgsql/data/postgresql.conf.orig    2010-08-20 15:23:50.000000000 +0
    +++ /var/lib/pgsql/data/postgresql.conf 2010-08-20 15:24:41.000000000 +0900
    @@ -56,7 +56,7 @@
    
     # - Connection Settings -
    
    -#listen_addresses = 'localhost'                # what IP address(es) to listen;
    +listen_addresses = '*'         # what IP address(es) to listen on;
                                            # comma-separated list of addresses;
                                            # defaults to 'localhost', '*' = all
                                            # (change requires restart)
    -bash-3.2$ cp /var/lib/pgsql/data/pg_hba.conf \
     /var/lib/pgsql/data/pg_hba.conf.orig
    -bash-3.2$ vi /var/lib/pgsql/data/pg_hba.conf
    -bash-3.2$ diff -uNr /var/lib/pgsql/data/pg_hba.conf.orig \
     /var/lib/pgsql/data/pg_hba.conf
    --- /var/lib/pgsql/data/pg_hba.conf.orig        2010-08-20 15:26:04.000000000 +0
    +++ /var/lib/pgsql/data/pg_hba.conf     2010-08-20 15:27:07.000000000 +0900
    @@ -72,3 +72,6 @@
     host    all         all         127.0.0.1/32          ident
     # IPv6 local connections:
     host    all         all         ::1/128               ident
    +# IPv4 local network connections:
    +host    pgtest      pgtest      192.168.1.0/24        md5
    +
    -bash-3.2$ exit
    logout
    [root@queen-centos ~]# service postgresql restart
    postgresql サービスを停止中:                               [  OK  ]
    postgresql サービスを開始中:                               [  OK  ]
    [root@queen-centos ~]# iptables -I INPUT -p tcp --dport 5432 -j ACCEPT
    [root@queen-centos ~]# service iptables save
    ファイアウォールのルールを /etc/sysconfig/iptables に保存中[  OK  ]
    [root@queen-centos ~]#
    

    PL/pgSQL デバッガのインストール

    本家からダウンロードできる tar ball のソースは PostgreSQL-8.4 には古いのですが、かといって CVS 版も 64bit 環境だと微妙にコケるので、パッチをあてた SRPM を置いておきます:

    以下のようにビルドしてインストールしてください:

    [root@queen-centos ~]# yum install -y rpm-build gcc \
     postgresql84-devel openssl-devel
    [root@queen-centos ~]# rpmbuild --rebuild \
     postgresql84-pldebugger-8.4.4-0.cvs20100919.src.rpm
    [root@queen-centos ~]# rpm -Uvh \
     /usr/src/redhat/RPMS/x86_64/postgresql84-pldebugger-8.4.4-0.cvs20100919.x86_64.rpm
    [root@queen-centos ~]#
    

    PL/pgSQL 用のデバッグプラグインがロードされるように設定ファイルを書きかえ、PostgreSQL を再起動します。

    [root@queen-centos ~]# cp /var/lib/pgsql/data/postgresql.conf \
     /var/lib/pgsql/data/postgresql.conf.orig2
    [root@queen-centos ~]# vi /var/lib/pgsql/data/postgresql.conf
    [root@queen-centos ~]# diff -uNr /var/lib/pgsql/data/postgresql.conf.orig2 \
     /var/lib/pgsql/data/postgresql.conf
    --- /var/lib/pgsql/data/postgresql.conf.orig2    2010-09-20 17:33:05.000000000 +0900
    +++ /var/lib/pgsql/data/postgresql.conf 2010-09-20 17:34:28.000000000 +0900
    @@ -121,7 +121,7 @@
    
     #max_files_per_process = 1000          # min 25
                                            # (change requires restart)
    -shared_preload_libraries = ''          # (change requires restart)
    +shared_preload_libraries = '$libdir/plugins/plugin_debugger'
    
     # - Cost-Based Vacuum Delay -
    
    [root@queen-centos ~]# service postgresql restart
    postgresql サービスを停止中:                               [  OK  ]
    postgresql サービスを開始中:                               [  OK  ]
    [root@queen-centos ~]#
    

    pldbgapi.sql を読み込んで、デバッグ用の型と共有ライブラリへのコネクタを定義し、PL/pgSQL 言語を組み込めば、サーバ側は準備完了です。

    [root@queen-centos ~]# psql -h queen-centos.priv -p 5432 -U pgtest pgtest
    ユーザ pgtest のパスワード:
    psql (8.4.4)
    "help" でヘルプを表示します.
    
    pgtest=# \i /usr/share/pgsql/contrib/pldbgapi.sql
    (中略)
    pgtest=# create language 'plpgsql';
    CREATE LANGUAGE
    pgtest=# \q
    [root@queen-centos ~]#
    

    なお、下記で CVS からのソースの anonymous ダウンロードができます:

    [root@queen-centos ~]# yum install -y cvs
    (中略)
    [root@queen-centos ~]# cvs \
     -d :pserver:anonymous@cvs.pgfoundry.org:/cvsroot/edb-debugger login
    [root@queen-centos ~]# cvs \
     -d :pserver:anonymous@cvs.pgfoundry.org:/cvsroot/edb-debugger checkout server
    [root@queen-centos ~]#
    

    バージョン違いでビルドできない場合:

    edb-debugger のソースは、本来は PostgreSQL のソースツリーの contrib/ に入れてビルドするものですが、今回は postgresql84-server をそのまま使いたかったので、分離してあります。しかし、postgresql84-devel パッケージは、残念ながら PL/pgSQL のヘッダを含んでいませんので、上記 SRPM は、postgresql84 の SRPM から持ってきた plpgsql.h を含んでいます。そのため、本体 (postgresql84) のマイナーバージョンが進んだら、適宜ヘッダファイルを更新して作り直す必要があります。

    参考:

    なお、これらの手順が面倒で、なおかつ RPM でのパッケージ管理がなくても良いのであれば、下記よりダウンロードできる EnterpriseDB 社の Postgres Plus のパッケージを入れるのも手です:

    デバッガの開発元ですので、普通にデバッガを利用できる状態でインストールできます。

    pgAdmin-III のインストール

    PostgreSQL の GUI クライアントである pgAdmin-III は CentOS の標準レポジトリに入っていませんので、EPEL から入れます。この例では、別のホストにインストールしています。

    [root@queen-centos2 ~]# rpm -Uvh $(printf \
     ftp://download.fedora.redhat.com/pub/epel/%s/%s/epel-release-*-*.noarch.rpm \
     $(rpm -q --qf "%{version}" $(rpm -q --whatprovides redhat-release)) \
     $(uname --hardware-platform) )
    (中略)
    [root@queen-centos2 ~]# yum install -y pgadmin3
    (中略)
    [root@queen-centos2 ~]#
    

    参考:

    デバッグをしてみる

    メニューから pgAdmin-III を実行します:

    メニュー

    データベースに接続し、適当なストアドプロシージャかトリガを作成します。そこにブレークポイントを設定します:

    ブレークポイントの選択

    その関数がどこかから呼ばれるまで待機します:

    デバッグセッションが待機中

    関数が呼ばれると実行が停止され、ステップ実行が可能になります:

    ステップ実行中




    ソース斜め読み: java.util.regex

    java.util.regex の概要をおさえるために書いたメモです。あまり記事の体をなしていなくて申し訳ありません。

    関連項目:

    Pattern, Matcher 共に、パッケージ java.util.regex に属します。

    Pattern のインスタンス化は、static ファクトリメソッド java.util.regex.Pattern#compile() (public static Pattern compile(String regex)) が行なうので、Pattern のコンストラクタは private です。

    Matcher のインスタンス化は、ファクトリメソッド java.util.regex.Pattern#matcher() (public Matcher matcher(CharSequence input)) が行なうので、java.util.regex.Matcher のコンストラクタはパッケージ private です。

    NFA によるバックトラックでマッチを行ないます。DFA は作りません。NFA ならばバックトラックなので、たとえば選択 (“|”) の各項目で条件に重複があっても構わずバックトラックすれば良いのですが、DFA ではそうは行かないはずです (DFA では、特定の入力に対して、状態遷移先は 1 つに決まらなければならないので)。grep(1) などは DFA を作っているようですね、今度見てみます。

    “static class Pattern.Node” がなすツリーは、いわゆる Composite パターンで、Node.matcher() は、どのノードでも呼べます。正規表現の文字クラスを表す “private static abstract class Pattern.CharProperty extends Node” では CharProperty.isSatisfiedBy(int ch) がそうです。

    なお、「普通の」内部クラスは、「インスタンスの」内部クラス。「クラスの」内部クラスにする (static メソッドから使えるようにする) には、”static” をつける必要があります。”Node” クラスは static ファクトリでインスタンス化されますので、static である必要がありました。C++ にはローカルクラスなど無かったので、新鮮です。

    デバッグ出力が見づらくてかなわないです。こんな具合に書き換えれば良い?:

        /**
         * Used to print out a subtree of the Pattern to help with debugging.
         */
        private static String INDENT = "  ";
        private static void printObjectTree(Node node, String indent) {
            while(node != null) {
                if (node instanceof Prolog) {
                    System.out.println(indent + "**** start contents prolog loop");
                    System.out.println(indent + node);
                    printObjectTree(((Prolog)node).loop, indent + INDENT);
                    System.out.println(indent + "**** end contents prolog loop");
                } else if (node instanceof Loop) {
                    System.out.println(indent + "**** start contents Loop body");
                    System.out.println(indent + node);
                    printObjectTree(((Loop)node).body, indent + INDENT);
                    System.out.println(indent + "**** end contents Loop body");
                } else if (node instanceof Curly) {
                    System.out.println(indent + "**** start contents Curly body");
                    System.out.println(indent + node);
                    printObjectTree(((Curly)node).atom, indent + INDENT);
                    System.out.println(indent + "**** end contents Curly body");
                } else if (node instanceof GroupCurly) {
                    System.out.println(indent +
                     "**** start contents GroupCurly body" );
                    System.out.println(indent + node);
                    printObjectTree(((GroupCurly)node).atom, indent + INDENT);
                    System.out.println(indent +
                     "**** end contents GroupCurly body" );
                } else if (node instanceof GroupTail) {
                    System.out.println(indent + node);
                    System.out.println(indent + "Tail next is "+node.next);
                    return;
                } else if (node instanceof Branch) {
                    System.out.println(indent + "**** start contents Branch body");
                    for (Node atom: ((Branch) node).atoms) {
                        printObjectTree(atom, indent + INDENT);
                    }
                    System.out.println(indent + "**** end contents Branch body");
                } else {
                    System.out.println(indent + node);
                }
                node = node.next;
                if (node != null)
                    System.out.println(indent + "->next:");
                if (node == Pattern.accept) {
                    System.out.println(indent + "Accept Node");
                    node = null;
                }
           }
        }
    
        private static void printObjectTree(Node node) {
            printObjectTree(node, "");
        }
    

    “a+|b+” で、バララとこんな感じに出ました。だいぶ分かりやすくなった:

    **** start contents Branch body
      **** start contents Curly body
      com.ayutaya.java.util.regex.Pattern$Curly@276af2
        com.ayutaya.java.util.regex.Pattern$Single@1de3f2d
        ->next:
        Accept Node
      **** end contents Curly body
      ->next:
      com.ayutaya.java.util.regex.Pattern$BranchConn@5d173
      ->next:
      com.ayutaya.java.util.regex.Pattern$LastNode@1f9dc36
      ->next:
      Accept Node
      **** start contents Curly body
      com.ayutaya.java.util.regex.Pattern$Curly@e86da0
        com.ayutaya.java.util.regex.Pattern$Single@1754ad2
        ->next:
        Accept Node
      **** end contents Curly body
      ->next:
      com.ayutaya.java.util.regex.Pattern$BranchConn@5d173
      ->next:
      com.ayutaya.java.util.regex.Pattern$LastNode@1f9dc36
      ->next:
      Accept Node
    **** end contents Branch body
    ->next:
    Accept Node
    

    では。




    ブート時の jar はどこに記述が?

    JDK-1.5 のドキュメントの「クラスの検索方法」) を見ると、以下のようにあります:

    ブートストラップクラスは、Java 2 プラットフォームを実装しているクラスです。ブートストラップクラスは、jre/lib ディレクトリの rt.jar と他のいくつかの JAR ファイルに格納されています。これらのアーカイブは、システムプロパティ sun.boot.class.path に格納されているブートストラップクラスパスの値によって指定されます。このシステムプロパティは参照専用なので、直接修正しないでください。

    ブートストラップクラスパスの再定義が必要になることはほとんどありません。まれに、別のコアクラスのセットを使用する必要が生じた場合には、非標準のオプション -Xbootclasspath を使ってブートストラップクラスパスを再定義することができます。

    すると、初期化時に勝手に読まれるというブートストラップのアーカイブ名のリストというのは、一体どこに記述されているのでしょうか? 起動側のラッパ? JRE のランチャ? それとも JVM の中?

    以下は、CentOS-5.5 収録の OpenJDK-1.6.0 での話になります。

    とりあえずは、システムプロパティを表示してもらいます (参考: 「Javaのシステムプロパティをすべて表示するJavaコード – いろいろ解析日記」。Properties.list() を使うと、value の出力が省略されてしまう…)。

    import java.util.Properties;
    
    class SystemProps {
        public static void main(String[] args) {
            Properties properties = System.getProperties();
            for (Object key: properties.keySet()) {
                System.out.println(key + ": " + properties.get(key));
            }
        }
    }
    

    関係ありそうなのは、”sun.boot.class.path” だけです。以下に見られるように、明示的には何も指定しなくても、どこかで値が設定されています。

    $ java SystemProps
    java.runtime.name: OpenJDK Runtime Environment
    sun.boot.library.path: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386
    java.vm.version: 14.0-b16
    java.vm.vendor: Sun Microsystems Inc.
    java.vendor.url: http://java.sun.com/
    path.separator: :
    java.vm.name: OpenJDK Client VM
    file.encoding.pkg: sun.io
    sun.java.launcher: SUN_STANDARD
    user.country: JP
    sun.os.patch.level: unknown
    java.vm.specification.name: Java Virtual Machine Specification
    user.dir: /home/knaka/src/java
    java.runtime.version: 1.6.0_0-b16
    java.awt.graphicsenv: sun.awt.X11GraphicsEnvironment
    java.endorsed.dirs: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/endorsed
    os.arch: i386
    java.io.tmpdir: /tmp
    line.separator:
    
    java.vm.specification.vendor: Sun Microsystems Inc.
    os.name: Linux
    sun.jnu.encoding: UTF-8
    java.library.path:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386/client:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/../lib/i386:
     /usr/java/packages/lib/i386:/lib:/usr/lib
    java.specification.name: Java Platform API Specification
    java.class.version: 50.0
    sun.management.compiler: HotSpot Client Compiler
    os.version: 2.6.18-194.8.1.el5
    user.home: /home/knaka
    user.zoneinfo.dir: /usr/share/javazi
    user.timezone:
    java.awt.printerjob: sun.print.PSPrinterJob
    file.encoding: UTF-8
    java.specification.version: 1.6
    java.class.path: .
    user.name: knaka
    java.vm.specification.version: 1.0
    java.home: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre
    sun.arch.data.model: 32
    user.language: ja
    java.specification.vendor: Sun Microsystems Inc.
    java.vm.info: mixed mode
    java.version: 1.6.0_0
    java.ext.dirs:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/ext:/usr/java/packages/lib/ext
    sun.boot.class.path:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/resources.jar:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/rt.jar:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/sunrsasign.jar:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/jsse.jar:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/jce.jar:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/charsets.jar:
     /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/classes
    java.vendor: Sun Microsystems Inc.
    file.separator: /
    java.vendor.url.bug: http://java.sun.com/cgi-bin/bugreport.cgi
    sun.io.unicode.encoding: UnicodeLittle
    sun.cpu.endian: little
    sun.cpu.isalist:
    $
    

    “rpm -ql” から “xargs grep” しても無いようなので、ラッパや設定ファイルに記述されているわけではなさそうです。続いて、以下のようにランチャのデバッグ出力を見ても、それらしい記述はありません。

    $ _JAVA_LAUNCHER_DEBUG= java HelloWorld
    ----_JAVA_LAUNCHER_DEBUG----
    Command line Args:
            argv[0] = 'java'
            argv[1] = 'HelloWorld'
    JRE path is /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre
    jvm.cfg[0] = ->-client<-
        name: -client  vmType: VM_IF_SERVER_CLASS  server_class: -server
    jvm.cfg[1] = ->-server<-
    jvm.cfg[2] = ->-hotspot<-
        name: -hotspot  vmType: VM_ALIASED_TO  alias: -client
    jvm.cfg[3] = ->-classic<-
    jvm.cfg[4] = ->-native<-
    jvm.cfg[5] = ->-green<-
    jvm.cfg[6] = ->-cacao<-
    jvm.cfg[7] = ->-zero<-
    1 micro seconds to parse jvm.cfg
    pages: 258766  page_size: 4096  physical memory: 1059905536 (0.987GB)
    linux_i386_ServerClassMachine: false
    Default VM: client
    Does `/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386/client/libjvm.so' exist ... yes.
    ----_JAVA_LAUNCHER_DEBUG----
    Command line Args:
            argv[0] = 'java'
            argv[1] = 'HelloWorld'
    JRE path is /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre
    jvm.cfg[0] = ->-client<-
        name: -client  vmType: VM_IF_SERVER_CLASS  server_class: -server
    jvm.cfg[1] = ->-server<-
    jvm.cfg[2] = ->-hotspot<-
        name: -hotspot  vmType: VM_ALIASED_TO  alias: -client
    jvm.cfg[3] = ->-classic<-
    jvm.cfg[4] = ->-native<-
    jvm.cfg[5] = ->-green<-
    jvm.cfg[6] = ->-cacao<-
    jvm.cfg[7] = ->-zero<-
    1 micro seconds to parse jvm.cfg
    pages: 258766  page_size: 4096  physical memory: 1059905536 (0.987GB)
    linux_i386_ServerClassMachine: false
    Default VM: client
    Does `/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386/client/libjvm.so' exist ... yes.
    JVM path is /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/i386/client/libjvm.so
    1 micro seconds to LoadJavaVM
    JavaVM args:
        version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 4
        option[ 0] = '-Djava.class.path=.'
        option[ 1] = '-Dsun.java.command=HelloWorld'
        option[ 2] = '-Dsun.java.launcher=SUN_STANDARD'
        option[ 3] = '-Dsun.java.launcher.pid=9020'
    1 micro seconds to InitializeJVM
    Main-Class is 'HelloWorld'
    Apps' argc is 0
    1 micro seconds to load main class
    ----_JAVA_LAUNCHER_DEBUG----
    Hello, World!
    $
    

    bootclasspath オプションに存在しないパッケージを渡すとコケるので、明示的に指定されなかったら、JVM の中で、デフォルトのパッケージを読むのだと思われます。

    $ java -Xbootclasspath:/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/foo.jar \
     HelloWorld
    Error occurred during initialization of VM
    java/lang/NoClassDefFoundError: java/lang/Object
    $ java -Xbootclasspath:/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/lib/rt.jar \
     HelloWorld
    Hello, World!
    $
    

    しかたがないので grep し倒してそれらしいところを探すと、どうやら os::set_boot_path() がそれです。スタックトレースをとってみます。

    $ gdb /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/bin/java
    (中略)
    (gdb) b os::set_boot_path
    Can't find member of namespace, class, struct, or union named "os::set_boot_path"
    Hint: try 'os::set_boot_path or 'os::set_boot_path
    (Note leading single quote.)
    Make breakpoint pending on future shared library load? (y or [n]) y
    Breakpoint 1 (os::set_boot_path) pending.
    (gdb) run HelloWorld
    Starting program: /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/jre/bin/java \
     HelloWorld
    (中略)
    [Switching to Thread 0xb7fefb90 (LWP 9549)]
    
    Breakpoint 1, os::set_boot_path (fileSep=47 '/', pathSep=58 ':')
        at /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/runtime/os.cpp:866
    866         const char* home = Arguments::get_java_home();
    (gdb) bt
    #0  os::set_boot_path (fileSep=47 '/', pathSep=58 ':') at \
     /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/runtime/os.cpp:866
    #1  0x00fbae77 in os::init_system_properties_values () at \
     /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/os/linux/vm/os_linux.cpp:332
    #2  0x00cf996f in Arguments::init_system_properties () at \
     /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/runtime/arguments.cpp:153
    #3  0x01058cb8 in Threads::create_vm (args=0xb7fef3a4, canTryAgain=0xb7fef30b) at \
     /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/runtime/thread.cpp:2820
    #4  0x00eba5f3 in JNI_CreateJavaVM (vm=0xb7fef3b8, penv=0xb7fef3b4, args=0xb7fef3a4) at \
     /usr/src/debug/icedtea6-1.6/openjdk/hotspot/src/share/vm/prims/jni.cpp:3263
    #5  0x0804a088 in InitializeJVM (_args=0xbfffe078) at \
     ../../../../src/share/bin/java.c:1296
    #6  JavaMain (_args=0xbfffe078) at ../../../../src/share/bin/java.c:431
    #7  0x00b09832 in start_thread () from /lib/libpthread.so.0
    #8  0x001e1e0e in clone () from /lib/libc.so.6
    (gdb)
    

    C で書かれた「ランチャ」 (エントリポイントは jdk/src/share/bin/java.c) から走るスレッドで、JNI_CreateJavaVM() (libjvm.so 内の関数。この先は C++) の中から呼ばれています。

    os.cpp を見ると、以下のようなリストでハードコードされています。ホルダ “%” が、システムプロパティ “java.home” に置換されるのでしょう。

    bool os::set_boot_path(char fileSep, char pathSep) {
        ...
        static const char classpath_format[] =
            "%/lib/resources.jar:"
            "%/lib/rt.jar:"
            "%/lib/sunrsasign.jar:"
            "%/lib/jsse.jar:"
            "%/lib/jce.jar:"
            "%/lib/charsets.jar:"
            "%/classes";
        char* sysclasspath = \
         format_boot_path(classpath_format, home, home_len, fileSep, pathSep);
        if (sysclasspath == NULL) return false;
        Arguments::set_sysclasspath(sysclasspath);
    
        return true;
    }
    

    だいぶ構造が見えてきました。では。




    Java 標準ライブラリをハックする

    Java で、標準ライブラリのコピーをソースから作り、好きにイジる手順です。どなたか、他にもっと効率的な方法をご存知でしたら教えていただけるとありがたいです (その意味では Python は、自己責任において、アクセス指定関係なくクラス内部を触り放題なので、好もしい)。

    まずは、こんなソースを用意します (注: コンパイルできません):

    import java.util.regex.*;
    
    class RegExpDebug  {
        public static void main(String args[]) {
            Pattern pattern = Pattern.compile(args[0]);
            pattern.printObjectTree(pattern.matchRoot);
            Matcher matcher = pattern.matcher(args[1]);
            while (matcher.find()) {
                System.out.println("Matched: " + matcher.group());
            }
        }
    }
    

    引数に渡された文字列で正規表現マッチングを行なうだけの、簡単な Java のコードです。Pattern.compile() に渡された正規表現が内部でどのような構造になっているかを表示する Pattern.printObjectTree() を呼びたいのですが、パッケージプライベートのデバッグ用なので、こちらからは呼べません。当然、コンパイルできません:

    $ CLASSPATH=./ javac RegExpDebug.java
    RegExpDebug.java:6: java.util.regex.Pattern の matchRoot は public ではありません。
     パッケージ外からはアクセスできません。
            pattern.printObjectTree(pattern.matchRoot);
                                           ^
    RegExpDebug.java:6: printObjectTree(java.util.regex.Pattern.Node) は
     java.util.regex.Pattern で private アクセスされます。
            pattern.printObjectTree(pattern.matchRoot);
                   ^
    エラー 2 個
    $
    

    仕方が無いので、パッケージ名に適当なプレフィクス (ここでは “com.ayutaya”) をつけて、新しいパッケージとしてコピーを作り、コンパイル終了の際に Pattern.printObjectTree() を呼ぶようにライブラリ側を修正します。新しい呼び出し側は、以下のような感じです:

    import com.ayutaya.java.util.regex.*;
    
    class RegExpDebug  {
        pubelic static void main(String args[]) {
            Pattern pattern = Pattern.compile(args[0]);
            Matcher matcher = pattern.matcher(args[1]);
            while (matcher.find()) {
                System.out.println("Matched: " + matcher.group());
            }
        }
    }
    

    当然、まだパッケージがないのでコンパイルはコケます:

    $ CLASSPATH=./ javac RegExpDebug.java
    RegExpDebug.java:1: パッケージ com.ayutaya.java.util.regex は存在しません。
    (中略)
    $
    

    パッケージのコピーを作成します。下記の例は、CentOS-5.5 上でのものですので、適宜読み替えてください。まずは、ライブラリのソースを入手します。Red Hat 系の OpenJDK ですと、java-*-openjdk-src に入っていると思います。

    $ sudo yum install -y java-1.6.0-openjdk-src
    (中略)
    $ rpm -ql java-1.6.0-openjdk-src
    /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/src.zip
    /usr/share/doc/java-1.6.0-openjdk-src-1.6.0.0
    /usr/share/doc/java-1.6.0-openjdk-src-1.6.0.0/README.src
    $
    

    コピーを作ってからパッケージ名を変更し、修正・コンパイルします。

    $ mkdir -p com/ayutaya/
    $ pushd com/ayutaya/
    ~/src/java/regexp/com/ayutaya ~/src/java/regexp
    $ unzip /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/src.zip "java/util/regex/*"
    Archive:  /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/src.zip
       creating: java/util/regex/
      inflating: java/util/regex/ASCII.java
      inflating: java/util/regex/Matcher.java
      inflating: java/util/regex/MatchResult.java
      inflating: java/util/regex/Pattern.java
      inflating: java/util/regex/PatternSyntaxException.java
    $ cd java/util/regex/
    $ perl -p -i \
     -e 's/package java.util.regex;/package com.ayutaya.java.util.regex;/' *.java
    $ cp Pattern.java Pattern.java.orig
    $ vi Pattern.java
    $ diff -uNr Pattern.java.orig Pattern.java
    --- Pattern.java.orig   2010-09-12 00:31:46.000000000 +0900
    +++ Pattern.java        2010-09-12 00:32:29.000000000 +0900
    @@ -1503,6 +1503,7 @@
             groupNodes = null;
             patternLength = 0;
             compiled = true;
    +        printObjectTree(matchRoot);
         }
    
         /**
    $ javac *.java
    (中略)
    $ popd
    ~/src/java/regexp
    $
    

    今度は無事コンパイルが通り、実行すると、内部構造がダンプされるようになります。

    $ CLASSPATH=./ javac RegExpDebug.java
    $ CLASSPATH=./ java RegExpDebug "a+" "xxxaaaxxx"
    com.ayutaya.java.util.regex.Pattern$Curly@fa3ac1
    com.ayutaya.java.util.regex.Pattern$Single@276af2
    ->next:
    Accept Node
    **** end contents Curly body
    ->next:
    com.ayutaya.java.util.regex.Pattern$LastNode@1de3f2d
    ->next:
    Accept Node
    Matched: aaa
    $
    

    後は、やり放題です。では。




    CentOS: SRPM, debuginfo 入手

    備忘録として、CentOS で、yum の SRPM レポジトリと debuginfo RPM レポジトリを登録する方法です。Fedora ならば普通に使えるのに、CentOS だと入っていないんですよね。開発に使うな、ランタイムとしてだけ使え、という意図でしょう。

    [root@black-server ~] cat <<'EOF' > /etc/yum.repos.d/CentOS-Source.repo
    [base-source]
    name=CentOS-$releasever - Base Source Packages
    baseurl=http://mirror.centos.org/centos/$releasever/os/SRPMS/
    gpgcheck=1
    enabled=0
    gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-$releasever
    
    [updates-source]
    name=CentOS-$releasever - Updates Source Packages
    baseurl=http://mirror.centos.org/centos/$releasever/updates/SRPMS/
    gpgcheck=1
    enabled=0
    gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-$releasever
    EOF
    [root@black-server ~]# yum install -y yum-utils
    (中略)
    [root@black-server ~]#
    

    あとは一般ユーザでも、以下のようにすれば SRPM をダウンロード、リビルドすることができます。

    $ yumdownloader --source bash
    (中略)
    $ ls -l bash-*.src.rpm
    -rw-r--r-- 1 knaka knaka 4800501  4月 10  2009 bash-3.2-24.el5.src.rpm
    $ rpmbuild --rebuild bash-3.2-24.el5.src.rpm
    (中略)
    $
    

    ついでに、debuginfo パッケージの yum レポジトリも追加します。

    [root@black-server ~] cat <<'EOF' > /etc/yum.repos.d/CentOS-Debug.repo
    [debuginfo]
    name=CentOS-$releasever - DebugInfo
    baseurl=http://debuginfo.centos.org/$releasever/$basearch/
    gpgcheck=0
    enabled=0
    EOF
    [root@black-server ~]
    

    debug パッケージは GPG サインが施されていませんので、gpgcheck はオフにします。”enabled=0″ で、標準では disable にしてありますので、利用の際には以下のようにします。

    [root@black-server ~]# yum --enablerepo=debuginfo install -y bash-debuginfo
    (中略)
    [root@black-server ~]# rpm -q bash-debuginfo
    bash-debuginfo-3.2-21.el5
    [root@black-server ~]#
    

    ただいかんせん、debuginfo パッケージが、updates に完全には追随していないんですよね…。

    0003622: debuginfo packages out of date – CentOS Bug Tracker

    そんなときは、base のパッケージに戻すか、自分でソースからコンパイルするかしましょう。Red Hat Enterprise Linux (RHEL) はちゃんと更新していますので、RHEL のサブスクリプションを買うという手もありますw。




    正規表現で最長一致(する|しない)言語

    先日、Java の java.util.regex.Pattern のソースを読みながら、twitter で以下のようにつぶやきました:

    正規表現の大原則は最左最長一致だと思ってたんですが、Javaの”|”(Branch)のmatch()がatoms間の比較をしていない。どう見ても「最初一致」です。えー、これってOKなの?

    すると、@buri17 からツッコミが:

    @knaka perlでも正規表現の選択演算子は短絡評価だというのを昔どこかで読んだ気がするけど、ちがいましたかー。

    えっ? マジですか? 調べてみました。

    最長一致するもの

    grep:

    $ echo "XaabbX" | grep -o -E "a+|[ab]+"
    aabb
    $ echo "XaabbX" | grep -o -E "[ab]+|a+"
    aabb
    

    sed:

    $ echo "XaabbX" | sed -r -e 's/a+|[ab]+/foo/'
    XfooX
    $ echo "XaabbX" | sed -r -e 's/[ab]+|a+/foo/'
    XfooX
    

    gawk:

    $ echo "XaabbX" | gawk '{ sub("a+|[ab]+", "foo"); print }'
    XfooX
    $ echo "XaabbX" | gawk '{ sub("[ab]+|a+", "foo"); print }'
    XfooX
    

    ちなみに、秀丸エディタの正規表現検索でも最長一致します。正規表現ならば ED(1) だろって? すまん、使ったこともない。

    最長一致しないもの

    Perl:

    $ echo "XaabbX" | perl -n -e 's/a+|[ab]+/foo/; print'
    XfoobbX
    $ echo "XaabbX" | perl -n -e 's/[ab]+|a+/foo/; print'
    XfooX
    

    Python:

    $ python -c 'import re; print re.compile("a+|[ab]+").search("XaabbX").group(0)'
    aa
    $ python -c 'import re; print re.compile("[ab]+|a+").search("XaabbX").group(0)'
    aabb
    

    Ruby:

    $ ruby -e 'puts /a+|[ab]+/.match("XaabbX")'
    aa
    $ ruby -e 'puts /[ab]+|a+/.match("XaabbX")'
    aabb
    

    PHP:

    $ php -r 'preg_match("/a+|[ab]+/", "XaabbX", $m); echo $m[0] . "\n";'
    aa
    $ php -r 'preg_match("/[ab]+|a+/", "XaabbX", $m); echo $m[0] . "\n";'
    aabb
    

    grep で、Perl 正規表現オプションつき (PCRE?):

    $ echo "XaabbX" | grep -o --perl-regexp "a+|[ab]+"
    aa
    bb
    $ echo "XaabbX" | grep -o --perl-regexp "[ab]+|a+"
    aabb
    

    ついでに、Emacs の検索も最長一致しません。

    まとめると

    当たり前だと思っていた正規表現の最左最長一致を、新しいめのツール・言語では行なっていないことが分かります。たしかに最長一致を探そうとすると、もし選択の中でマッチが見つかっても、最長のマッチを探すために他の全ての項も試さなければなりませんから、実装的にも実行時にも高コストなのは分かりますが、気をつけねばなりませんな。

    古いめのツールでは行なうということは、これって POSIX の正規表現の規格にも反映されていたりするのか? と思って、PHP でも古い方の関数で試してみると、下記の通り、こちらは最長一致するんですよね。参考: 「PHP: POSIX 正規表現関数 – Manual

    PHP の POSIX 正規表現関数 (PHP >= 5.3 非推奨):

    $ php -r 'ereg("a+|[ab]+", "XaabbX", $m); echo $m[0] . "\n";'
    aabb
    $ php -r 'ereg("[ab]+|a+", "XaabbX", $m); echo $m[0] . "\n";'
    aabb
    

    そこで “POSIX 最長一致” でググると、こんな資料が → 「PHPの正規表現と
    最長一致, hanawa (a.k.a. id:hnw), y at hnw dot jp, 第29回PHP勉強会発表資料
    」 (PDF)。よく「最長マッチ」と誤訳される「Greedy-matching (繰り返しで最長まで食ってしまうマッチ)」と「Longest-matching (最長マッチ)」は別物よ、ということで、参考になりました。

    では。




    Ruby Mix-in で菱形継承

    Ruby はご存知のとおり、is-a 関係で継承を行なう「普通の」継承と、付加機能としてインターフェイスと実装を引き継ぐためのミックスイン継承とを備えています。通常継承では単一継承しか許していない Ruby ですが、通常の継承とミックスインとの多重継承、ミックスインとミックスインとの多重継承はできます。オーバーライドした際の同一シグネチャメソッドの優先順位がどうなっているのか気になったので、やってみました。

    予想としては、当然通常継承の方が優先度が高いでしょうし、ミックスイン間であれば、”include” などというくらいですから、当然後から include した方が上書きしそうです。実際そうでした。やるまでもなかったかも知れません。

    #!/usr/bin/ruby
    # -*- coding: utf-8 -*-
    
    module BaseMixin
      def initialize
        print "BaseMixin initialized\n"
      end
    end
    
    module DerivedMixin1
      include BaseMixin
      def initialize
        super
        print "DerivedMixin1 initialized\n"
      end
      def foo
        print "foo@DerivedMixin1 called\n"
      end
      def bar
        print "bar@DerivedMixin1 called\n"
      end
    end
    
    module DerivedMixin2
      include BaseMixin
      def initialize
        super
        print "DerivedMixin2 initialized\n"
      end
      def foo
        print "foo@DerivedMixin2 called\n"
      end
      def bar
        print "bar@DerivedMixin2 called\n"
      end
    end
    
    class Base
      include DerivedMixin1
      include DerivedMixin2
      def initialize
        super
        print "Base initialized\n"
      end
    end
    
    class Derived < Base
      include DerivedMixin2
      include DerivedMixin1
      def initialize
        super
        print "Derived initialized\n"
      end
      def bar
        print "bar@Derived called\n"
      end
    end
    
    o = Derived.new
    o.foo
    o.bar
    

    実行結果:

    $ ./diamond.rb
    BaseMixin initialized
    DerivedMixin1 initialized
    DerivedMixin2 initialized
    Base initialized
    Derived initialized
    foo@DerivedMixin2 called
    bar@Derived called
    

    Mix-in のコンストラクタは include した順(登録された順?)に実行され、その後で親クラスのコンストラクタが走ります。基底と派生とで include していたら、基底で include された方が優先で、派生の方での include は無視。これも妥当と思われる。

    名前かぶりは、後 include の Mix-in 優先、基底クラスがより優先、は予想通り。

    型も動的なので、菱形継承した基底の “BaseMixin” のインスタンスは一つしかありませんのね、C++ で言うところの “virtual public” 継承。初期化パラメータが両経路で異なったりすると混乱しそうなので、Mix-in の基底には複雑なことはやらせないようにしようと思います。Python も確かこんなだったような気がします。今度やってみます。

    では。




    マークアップ言語主要書式

    混乱したり忘れたりをよくするので、各種マークアップ言語における、主要な書式をまとめておきます。まともな構造化文書を書くのに、下記の要素程度をおさえておけばだいたい足りると思うんですよね (数式や楽譜は別)。

    • 表題の階層
    • 段落
    • 整形なし
    • 順序なし箇条書き
    • 順序あり箇条書き
    • 画像の貼り付け
    • URL リンク
    • 独自リンク
    • 表組み

    # roff (nroff) も要ります??

    LaTeX

    • 表題の階層: \section{~}, \subsection{~}, \subsubsection{~}
    • 段落: 一行以上あける
    • 整形なし: \begin{verbatim}
    • 順序なし箇条書き: \begin{itemize} → \item
    • 順序あり箇条書き: \begin{enumerate} → \item
    • 画像の貼り付け: \usepackage{graphicx} \includegraphics{~.eps}
    • URL リンク: N/A
    • 独自リンク: N/A
    • 表組み:
    \begin{tabular}{lll}
    \hline
    header 1 & header 2 & header 3\\
    \hline \\
    data 1 & data 2 & data 3 \\
    \hline
    \end{tabular}
    

    HTML

    • 表題の階層: <h1>~<h6>
    • 段落: <p>
    • 整形なし: <pre>
    • 順序なし箇条書き: <ul> → <li>
    • 順序あり箇条書き: <ol> → <li>
    • 画像の貼り付け: <img>
    • URL リンク: <a>
    • 独自リンク: N/A
    • 表組み:
    <table border=1>
      <caption align=bottom>caption</caption>
      <tr><th>header 1</th><th>header 2</th><th>header 3</th></tr>
      <tr><th>data 1</th><td>data 2</td><td>data 3</td></tr>
      <tr><td rowspan=2>data 1</td><td colspan=2>data 2</td></tr>
      <tr><td>data 2</td><td>data 3</td></tr>
    </table>
    

    PukiWiki

    • 表題の階層: *, **, ***
    • 段落: 一行以上あける
    • 整形なし: 1 スペース下げ
    • 順序なし箇条書き: -, –, —
    • 順序あり箇条書き: +, ++, +++
    • 画像の貼り付け: &ref(添付ファイル名), &ref(URL)
    • URL リンク: [[エイリアス名>URL]]
    • 独自リンク: [[ページ名]]
    • 表組み:
    |CENTER: header 1|CENTER: header 2|CENTER: header3|h
    |RIGHT:|LEFT:|LEFT:|c
    |~data 1|data 2|data 3|
    |data 1|>|data 3|
    |~|data 2|data 3|
    

    MediaWiki

    • 表題の階層: =1=, ==2==, ===3===, ====4====, =====5=====, ======6======
    • 整形なし: 1 スペース下げ
    • 段落: 一行以上あける
    • 順序なし箇条書き: * を連ねる
    • 順序あり箇条書き: # を連ねる
    • 画像の貼り付け: [[File:ファイル名]]
    • URL リンク: [URL エイリアス名]
    • 独自リンク: [[ページ名]]
    • 表組み:
    {| border="1"
    |+ align="bottom" | caption
    !header 1
    !header 2
    !header 3
    |-
    |data 1
    |data 2
    |data 3
    |}
    

    WordPress (HTML 編集モード)

    • 表題の階層: サイト名が <h1>、表題が <h2> を用いるので、本文は <h3> から
    • 段落: 改行する
    • (以下は “HTML” と同様)



    ワイルドカード関数を書く

    こんなお題が出たことがあります:

    • ワイルドカードによるマッチングを行なう関数を書きなさい
    • 引数には、ワイルドカードパターンとマッチ対象文字列をとり、成否を返します
    • ワイルドカードのルールは以下のとおり:
      • “?” は、任意の 1 文字にマッチします
      • “*” は、0 文字以上の文字列にマッチします
      • それ以外の文字は、その文字自身にマッチします

    まず単純に 1 パスのループで回しながらマッチングをすることを考えて、すぐに “*” の処理ができないことに気づいて行きづまりました。そして、一足とびに、正規表現マッチングのコードを思い出してしまったために、頭はすっかり非決定性有限オートマトンから決定性有限オートマトンを生成する方法ってどうやるんだったっけな、と明後日の方向へ行ってしまいました。

    けれどもよく考えたら、この程度のワイルドカードならばパーサは必要ないわけです。一般化すれば、「少なくとも一つの解を見つければ OK」という典型的な問題なんですよね。

    そんなわけで、ちょっと書いてみました。キモは、1 つの “*” が、対象文字列の 0 文字から残り全部の文字までを食った場合のそれぞれごとに再帰してバックトラック、というのに気づけるかどうか。

    #include <stdio.h>
    
    const int Success = 1;
    const int Failure = 0;
    
    int
    match (
      const char * pattern,
      const char * haystack ) {
        while ((* pattern) || (* haystack)) {
            // 先にどちらか尽きたら失敗
            if ((! (* pattern)) || (! (* haystack))) {
                return (Failure);
            }
            // 今回の "*" が 0 文字から残り全ての文字を食い尽くした場合ごと
            //  に再帰
            if ((* pattern) == '*') {
                for (int i = 0; (* (haystack + i)); ++ i) {
                    if (match(pattern + 1, haystack + i) == Success) {
                        return (Success);
                    }
                }
                return (Failure);
            // 1 文字ずつのマッチは簡単
            } else if (
             ((* pattern) == (* haystack)) ||
             ((* pattern) == '?') ) {
                pattern ++;
                haystack ++;
            // 1 文字単位で不一致ならば失敗
            } else {
                return (Failure);
            }
        }
        // 共に尽きたら成功
        return (Success);
    }
    
    int
    main (
      int argc,
      char * * argv) {
        if ((match(argv[1], argv[2])) == Failure) {
            printf("Not matched\n");
            return (1);
        }
        printf("Matched\n");
        return (0);
    }
    

    精進せねば。