Mercurial-2.6から追加されたunionスキームについて調べてみた。
Mercurial-2.6から追加されたunionスキームについて調べてみた。
Mercurial-2.6からリポジトリ指定時にunionというスキームが指定できるようになりました。 unionを利用すると、次の形式で指定した2つのローカルリポジトリの履歴をミックスして閲覧できるようになります。
union:repo1+repo2 # repo1とrepo2の履歴を合算して閲覧する
この機能を利用すれば、crew+main*1を手元で模倣できます。
$ hg clone http://hg.intevation.org/mercurial/ main $ hg clone http://hg.intevation.org/mercurial/crew/ crew $ MAIN_REV=$(hg -R main log -b default -l 1 --template "{node}") $ CREW_REV=$(hg -R crew log -b default -l 1 --template "{node}") $ hg -R union:crew+main heads $ hg -R union:crew+main log -r $CREW_REV -r $MAIN_REV $ hg -R union:crew+main log -r "::$CREW_REV-::$MAIN_REV" $ hg -R union:crew+main log -r "ancestor($CREW_REV,$MAIN_REV)" $ hg -R union:crew+main diff -r $CREW_REV -r $MAIN_REV
mercurial/unionrepo.pyのコミットコメントも使い方が書いてあります。参照してみてください。
なんでこんなものが追加されたの?
コミットコメントにある通り、RhodecodeやBitbucketなどのホスティングサービスで利用するような機能がCoreにあると便利だよね!ってことでRhodeCodeで利用されていたコードをMercurial本体に導入したみたいです。該当のコードはこちら。
Rhodecodeではリポジトリ同士の比較(rhodecode/controller/compare.py)やプルリクエスト(rhodecode/model/pull_request.py)などで利用されているようです。
Mercurial-2.6の並列になったupdateの注意点
いくつか注意点があることを id:flying-foozy さんに指摘してもらったので紹介します。
並列updateで10万規模のファイルを一度に更新するとlinux kernelでsoft lockupと誤認される
10万規模のファイルを更新するとlinux kernelでsoft lockupと誤認されるようです。
バグとして登録されているのですが、linux kernel側の問題ということで、mercurial側で対応する気はないようです。
soft lockupとはタスクやカーネルスレッドがCPUを一定時間専有している状態(一定時間割り込みが禁止されている状態)を指すらしく、 通常はカーネルのバグかハードウェアのバグで発生するみたいです。(遭遇したこと無い、、)
そもそも10万ファイルあるような巨大なリポジトリを利用するケースは少ないと思いますが、*1 この問題に遭遇した場合は、次で紹介するworker.numcpusで調整すると良いかもしれません。
@troter #mercurialjp 10万規模を一気にupdateする場合、条件次第ではLinuxカーネルの問題からsoft lockup扱いされるケースがある模様 bz.selenic.com/show_bug.cgi?i… なので、一応ブログに注記等を入れておいた方が良いかも?
— FUJIWARA Katsunoriさん (@flyingfoozy) 2013年4月24日
並列実行のworker数はhgrcのworker.numcpusで設定可能
並列実行のworker数はhgrcのworker.numcpusで設定可能です。
[worker] numcpus = 1 # 並列実行しない
未設定の場合、worker数はCPU数に応じて4から32の間になります。
@troter #mercurialjp カーネルパラメータを弄る手もあるみたいですが kb.vmware.com/selfservice/mi…Mercurial 側で worker.numcpus 設定値を明示的に1とかに下げておくのが妥当(&簡単)かな?
— FUJIWARA Katsunoriさん (@flyingfoozy) 2013年4月24日
非POSIX環境(≒Windows)では並列updateが利用できない
非POSIX環境ではworkerの起動コスト1e30という非常に大きな値にする事によって実質並列updateが行われないように調整されています。
@troter #mercurialjp もう一点注意事項を見つけました。非POSIX環境≒Win環境だと、並列実行されない模様 selenic.com/repo/hg/file/f… プロセス周りの挙動が違うしなぁと思ったら、そもそも win だと os.fork() がなかった。
— FUJIWARA Katsunoriさん (@flyingfoozy) 2013年4月28日
気になった点
よく見ると、この2つのif文の条件は両方ともif os.name == 'posix'で良い気がします。
Mercurial-2.6-rcの並列になったupdateをmozilla-centralで試す
rhodecodeのリポジトリよりずっと巨大なmozilla-centralでMercurial-2.6-rcの並列になったupdateを試してみた。
mozilla-centralの大きさはこんな感じ。13万リビジョン、7万8千ファイルある。
% hg tip リビジョン: 129561:aa620f3fc2f7 タグ: tip ユーザ: Matthew Noorenberghe <mozilla@noorenberghe.ca> 日付: Mon Apr 22 17:19:44 2013 -0700 要約: Bug 841967 - Use performance.now() for the popup notification security delay since it's monotonically increasing. r=dolske % hg manifest -r tip | wc -l 78109
ちなみに、リポジトリの取得には次のURLの方法を利用した。
速度を測ってみた
- mercurial-2.5.4
% hg up FIREFOX_AURORA_21_BASE; time hg up FIREFOX_AURORA_22_BASE 9911 files updated, 0 files merged, 8042 files removed, 0 files unresolved 14638 files updated, 0 files merged, 3315 files removed, 0 files unresolved hg up FIREFOX_AURORA_22_BASE 16.35s user 7.11s system 74% cpu 31.578 total % hg up FIREFOX_AURORA_21_BASE; time hg up FIREFOX_AURORA_22_BASE 9911 files updated, 0 files merged, 8042 files removed, 0 files unresolved 14638 files updated, 0 files merged, 3315 files removed, 0 files unresolved hg up FIREFOX_AURORA_22_BASE 15.97s user 6.35s system 74% cpu 29.815 total
- mercurial-2.6-rc
% hg up FIREFOX_AURORA_21_BASE; time hg up FIREFOX_AURORA_22_BASE ファイルの更新数 9911、 マージ数 0、 削除数 8042、 衝突未解消数 0 ファイルの更新数 14638、 マージ数 0、 削除数 3315、 衝突未解消数 0 hg up FIREFOX_AURORA_22_BASE 14.36s user 7.24s system 139% cpu 15.429 total % hg up FIREFOX_AURORA_21_BASE; time hg up FIREFOX_AURORA_22_BASE ファイルの更新数 9911、 マージ数 0、 削除数 8042、 衝突未解消数 0 ファイルの更新数 14638、 マージ数 0、 削除数 3315、 衝突未解消数 0 hg up FIREFOX_AURORA_22_BASE 13.67s user 6.74s system 149% cpu 13.652 total
平均と差分
;; mercurial-2.5.4 (/ (+ 31.578 29.815) 2) ; => 30.6965 ;; mercurial-2.6-rc (/ (+ 15.429 13.652) 2) ; => 14.5405 (- (/ (+ 31.578 29.815) 2) (/ (+ 15.429 13.652) 2)) ; => 16.156
倍以上早くなっている。巨大なリポジトリを扱う場合は効果が大きい。
(追記)注意点
Mercurial-2.6-rcの並列になったupdateを試す
多数のパフォーマンスチューニングを含むMercurial-2.6のリリース候補版がリリースされた。今回のリリースからhg updateの処理が並列化されるようになったので試してみた。
インストール
バージョンを指定してインストール。
% pip install -U mercurial==2.6-rc
hg updateの速度を見てみる
2.6からhg updateが並列で動くらしいのでrhodecodeのリポジトリで試してみる。
% hg clone https://bitbucket.org/marcinkuzminski/rhodecode $ cd rhodecode
- mercurial-2.5.4
% hg up null > /dev/null; time (hg up default > /dev/null && hg up beta > /dev/null) ( hg up default > /dev/null && hg up beta > /dev/null; ) 1.56s user 0.62s system 72% cpu 3.018 total % hg up null > /dev/null; time (hg up default > /dev/null && hg up beta > /dev/null) ( hg up default > /dev/null && hg up beta > /dev/null; ) 2.15s user 0.63s system 95% cpu 2.897 total % hg up null > /dev/null; time (hg up default > /dev/null && hg up beta > /dev/null) ( hg up default > /dev/null && hg up beta > /dev/null; ) 2.41s user 0.65s system 96% cpu 3.181 total % hg up null > /dev/null; time (hg up default > /dev/null && hg up beta > /dev/null) ( hg up default > /dev/null && hg up beta > /dev/null; ) 2.10s user 0.60s system 96% cpu 2.792 total
- mercurial-2.6-rc
% hg up null > /dev/null; time (hg up default > /dev/null && hg up beta > /dev/null) ( hg up default > /dev/null && hg up beta > /dev/null; ) 2.33s user 0.78s system 116% cpu 2.684 total % hg up null > /dev/null; time (hg up default > /dev/null && hg up beta > /dev/null) ( hg up default > /dev/null && hg up beta > /dev/null; ) 2.06s user 0.66s system 134% cpu 2.024 total % hg up null > /dev/null; time (hg up default > /dev/null && hg up beta > /dev/null) ( hg up default > /dev/null && hg up beta > /dev/null; ) 2.00s user 0.64s system 137% cpu 1.913 total % hg up null > /dev/null; time (hg up default > /dev/null && hg up beta > /dev/null) ( hg up default > /dev/null && hg up beta > /dev/null; ) 2.65s user 0.79s system 118% cpu 2.908 total
mercurial-2.6-rcのCPUの部分が100%を超えている。
- 平均と差分
;; mercurial-2.5.4 (/ (+ 3.018 2.897 3.181 2.792) 4) ; => 2.972 ;; mercurial-2.6-rc (/ (+ 2.684 2.024 1.913 2.908) 4) ; => 2.38225 (- (/ (+ 3.018 2.897 3.181 2.792) 4) (/ (+ 2.684 2.024 1.913 2.908) 4)) ; => 0.58975
この例だと、0.5秒ほど早くなっている。
(追記)注意点
DataMapperでOptimisticLock(楽観ロック)を利用する
dm-core-1.2.0の話です。
DataMapperでOptimisticLock(楽観ロック)を実現するプラグインはいくつか検索でヒットするのですが、古すぎて利用できなかったり、UPDATE文のWHERE句に条件が入るのではなく、プログラム側で判断しているためアトミック性がなかったりします。 ということでモンキーパッチです。*1
モンキーパッチ
module DataMapper class StaleObjectError < PersistenceError def initialize(query = nil) @query = query end def message if @query "Attempted to update a stale object: #{@query.model.name}" else "Attempted to update a stale object" end end end module Adapters # dm-do-adapter-1.2.0/lib/dm-do-adapter/adapter.rb DataObjectsAdapter.class_eval do DEFAULT_LOCKING_COLUMN = 'lock_version' alias_method :original_update, :update def update(attributes, collection) query = collection.query locking_property = query.model.properties(name)[locking_column.to_sym] if locking_property.blank? or collection.length != 1 return original_update(attributes, collection) end properties = [] bind_values = [] # make the order of the properties consistent query.model.properties(name).each do |property| next unless attributes.key?(property) properties << property bind_values << attributes[property] end # Add a condition to confirm whether a locking column is the same version. query = query.dup.update(locking_column.to_sym => collection.first.__send__(locking_column.to_sym)) properties << locking_property next_locking_column_value = collection.first.__send__(locking_column.to_sym) + 1 bind_values << next_locking_column_value statement, conditions_bind_values = update_statement(properties, query) bind_values.concat(conditions_bind_values) affected_rows = with_connection do |connection| connection.create_command(statement).execute_non_query(*bind_values) end.affected_rows unless affected_rows == 1 raise DataMapper::StaleObjectError.new(query) end collection.first.__send__("#{locking_column}=".to_sym, next_locking_column_value) affected_rows end def locking_column=(value) @original_locking_column = @locking_column if defined?(@locking_column) @locking_column = value.to_s end def locking_column reset_locking_column unless defined?(@locking_column) @locking_column end def reset_locking_column self.locking_column = DEFAULT_LOCKING_COLUMN end end end end
使い方
モデルにlock_versionというカラムを追加します。
class User include DataMapper::Resource property :id, Serial property :name, String property :lock_version, Integer, default: 0 end
モデルのlock_versionとDBのlock_versionの値と異なるとDataMapper::StaleObjectErrorが発生します。
a = User.create(name: 'troter') # INSERT INTO `users` (`name`, `lock_version`) VALUES ('troter', 0) # => #<User @id=1 @name="troter" @lock_version=0> a.name = 'TROTER' a.save # UPDATE `users` SET `name` = 'TROTER', `lock_version` = 1 WHERE (`id` = 1 AND `lock_version` = 0) a # => #<User @id=1 @name="TROTER" @lock_version=1> a.name = 'troter' a.lock_version = 0 a.save # UPDATE `users` SET `name` = 'troter', `lock_version` = 0, `lock_version` = 1 WHERE (`id` = 1 AND `lock_version` = 0) # DataMapper::StaleObjectError: DataMapper::StaleObjectError # from /home/troter/sandbox/datamapper-sandbox/project_name/config/initializers/data_mapper.rb:143:in `update' from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/repository.rb:180:in `update' # from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/resource/persistence_state/dirty.rb:54:in `update_resource' # from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/resource/persistence_state/dirty.rb:22:in `commit' # from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/resource.rb:956:in `_persist' # from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/resource.rb:987:in `block in update_with_hooks' # from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/resource.rb:984:in `catch' # from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/resource.rb:984:in `update_with_hooks' # ...省略
これで、安心して楽観ロックが利用できます。
*1:誰かgemにして。。
DataMapperのdm-migrationsのマイグレーションファイルの通し番号は重複していても問題ない
dm-migrations-1.2.0の話です。
dm-railsで次のようにdm-migrationsのマイグレーションファイルを生成すると、001から始まる通し番号のマイグレーションファイルが作成されます。
$ bundle exec rails generate migration create_users invoke data_mapper create db/migrate/001_create_users.rb
複数人で開発を行なっていると、データベースのマイグレーションスクリプトの番号が衝突しがちです。これは困ったなーとはじめのうちは通し番号の調整を行なっていたのですが、無駄な作業だという事がわかりました。
dm-migrationsのマイグレーション情報の管理方法
マイグレーションの適用状況を管理するテーブルmigration_infoのテーブル定義は次の通りです。
CREATE TABLE `migration_info` ( `migration_name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, UNIQUE KEY `migration_name` (`migration_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
migration_name、名前の方しか管理していません。実は通し番号はおまけで、名前が重要なのでした。
通し番号が重複した場合どうなるのか。
実際に重複している場合の動作を確認してみます。次のようなファイルを用意した場合のマイグレーションの適用状況を考えます。
- db/migrate/001_create_users.rb
# -*- coding: utf-8 -*- migration 1, :create_users do up do create_table :users do column :id, Integer, serial: true column :name, String column :created_at, DateTime column :updated_at, DateTime end end down do drop_table :users end end
- db/migrate/002_create_categories.rb
# -*- coding: utf-8 -*- migration 2, :create_categories do up do create_table :categories do column :id, Integer, serial: true column :name, String, size: 20, not_null: true column :created_at, DateTime column :updated_at, DateTime end end down do drop_table :categories end end
- db/migrate/002_create_movies.rb
# -*- coding: utf-8 -*- migration 2, :create_movies do up do create_table :movies do column :id, Integer, serial: true column :title, String, size: 255, not_null: true column :released_on, Date column :imdb_url, String, size: 255 column :created_at, DateTime column :updated_at, DateTime end end down do drop_table :movies end end
- db/migrate/003_create_category_movies.rb
# -*- coding: utf-8 -*- migration 3, :create_category_movies do up do create_table :category_movies do column :category_id, Integer, primary_key: true column :movie_id, Integer, primary_key: true end # auto_migrateではdm-constraintsの機能でFK制約が作成されるが # db-migrationsからその機能は利用できない。 create_index :category_movies, :category_id create_index :category_movies, :movie_id end down do drop_table :category_movies end end
bundle exec rake db:migrate
マイグレーションを実行してみましょう。
$ bundle exec rake db:migrate == Performing Up Migration #1: create_users CREATE TABLE `users` (`id` SERIAL PRIMARY KEY, `name` VARCHAR(50), `created_at` DATETIME, `updated_at` DATETIME) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT='' -> 0.1809s -> 0.1855s == Performing Up Migration #2: create_categories CREATE TABLE `categories` (`id` SERIAL PRIMARY KEY, `name` VARCHAR(20), `created_at` DATETIME, `updated_at` DATETIME) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT='' -> 0.2658s -> 0.2682s == Performing Up Migration #2: create_movies CREATE TABLE `movies` (`id` SERIAL PRIMARY KEY, `title` VARCHAR(255), `released_on` DATE, `imdb_url` VARCHAR(255), `created_at` DATETIME, `updated_at` DATETIME) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT='' -> 0.1742s -> 0.1772s == Performing Up Migration #3: create_category_movies CREATE TABLE `category_movies` (`category_id` INTEGER, `movie_id` INTEGER) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT='' -> 0.1572s CREATE INDEX `index_category_movies_category_id` ON `category_movies` (`category_id`) -> 0.1088s CREATE INDEX `index_category_movies_movie_id` ON `category_movies` (`movie_id`) -> 0.1344s -> 0.4025s
全部適用されます。migration_infoは次のとおり。
> select * from migration_info; +------------------------+ | migration_name | +------------------------+ | create_categories | | create_category_movies | | create_movies | | create_users | +------------------------+ 4 rows in set (0.00 sec)
bundle exec rake 'db:migrate:down[1]'
マイグレーションの状態を1に戻してみましょう
$ bundle exec rake 'db:migrate:down[1]' == Performing Down Migration #3: create_category_movies DROP TABLE `category_movies` -> 0.0310s -> 0.0312s == Performing Down Migration #2: create_movies DROP TABLE `movies` -> 0.0678s -> 0.0679s == Performing Down Migration #2: create_categories DROP TABLE `categories` -> 0.0429s -> 0.0430s
2が両方とも外されました。migration_infoは次のとおり。
> select * from migration_info; +-------------------+ | migration_name | +-------------------+ | create_users | +-------------------+ 1 rows in set (0.00 sec)
bundle exec rake 'db:migrate:up[2]'
2だけ適応してみましょう。
$ bundle exec rake 'db:migrate:up[2]' == Performing Up Migration #2: create_categories CREATE TABLE `categories` (`id` SERIAL PRIMARY KEY, `name` VARCHAR(20), `created_at` DATETIME, `updated_at` DATETIME) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT='' -> 0.1353s -> 0.1442s == Performing Up Migration #2: create_movies CREATE TABLE `movies` (`id` SERIAL PRIMARY KEY, `title` VARCHAR(255), `released_on` DATE, `imdb_url` VARCHAR(255), `created_at` DATETIME, `updated_at` DATETIME) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT='' -> 0.1231s -> 0.1254s
両方とも適用されました。migration_infoは次のとおり。
> select * from migration_info; +-------------------+ | migration_name | +-------------------+ | create_categories | | create_movies | | create_users | +-------------------+ 3 rows in set (0.00 sec)
名前が重複していた場合どうなるか。
名前が重複したケースも試してみましょう。
$ cp db/migrate/003_create_category_movies.rb db/migrate/004_create_category_movies.rb % bundle exec rake db:migrate rake aborted! Migration name conflict: 'create_category_movies' /home/troter/.rvm/gems/ruby-1.9.3-p327/gems/dm-migrations-1.2.0/lib/dm-migrations/migration_runner.rb:43:in `migration' db/migrate/004_create_category_movies.rb:3:in `<top (required)>' /home/troter/.rvm/gems/ruby-1.9.3-p327/gems/activesupport-3.2.13/lib/active_support/dependencies.rb:245:in `load' # ------------ 省略
コンフリクトという事でエラーになりました。
まとめ
DataMapperで比較的高速にidだけ取得する
dm-core-1.2.0の話です。
現在の検索条件でidだけ取得したい、、10000件くらい。って要望、よくあると思う。:fieldsで指定しても、モデルのインスタンスを作るため、sqlを直接実行した時にはかなわなかったりする。
なので、DataMapper::Collectionに次のようなメソッドを生やしている。
module DataMapper class Collection < LazyArray def select_statement(repository_name = :default) DataMapper.repository(repository_name).adapter.__send__(:select_statement, self.query) end def sql_select(repository_name = :default) return [] unless self.query.valid? sql, args = select_statement(repository_name) DataMapper.repository(repository_name).adapter.select(sql, *args) end end end
モデルのクラスメソッドにしないのは、クエリチェインするとクエリが壊れる恐れがある ため。
SQLとパラメータの取得方法についてはmonoさんの記事を参考しにした。
使い方
:fields と組み合わせて使う。 :fields で指定した要素が1つの場合はStructを作らずに値がそのまま入るので便利。
User.all(:name.like => 'a%').all(fields: [:id]).sql_select # => [1,2,3]