├── Gemfile ├── ext ├── libjio.tar.gz └── jio │ ├── ruby19.h │ ├── jruby.h │ ├── jio_prelude.h │ ├── rubinius.h │ ├── file.h │ ├── transaction.h │ ├── ruby18.h │ ├── jio_ext.h │ ├── extconf.rb │ ├── jio_ext.c │ ├── transaction.c │ └── file.c ├── lib ├── jio │ ├── version.rb │ └── file.rb └── jio.rb ├── .gitignore ├── Gemfile.lock ├── .travis.yml ├── test ├── test_jio.rb ├── helper.rb ├── test_file.rb └── test_transaction.rb ├── jio.gemspec ├── LICENSE ├── Rakefile └── README.rdoc /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gemspec 4 | 5 | gem 'rake' -------------------------------------------------------------------------------- /ext/libjio.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/methodmissing/jio/HEAD/ext/libjio.tar.gz -------------------------------------------------------------------------------- /lib/jio/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module JIO 4 | VERSION = '0.1' 5 | end -------------------------------------------------------------------------------- /lib/jio.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | begin 4 | require "jio/jio_ext" 5 | rescue LoadError 6 | require "jio_ext" 7 | end 8 | 9 | module JIO 10 | end 11 | 12 | require 'jio/file' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .libs/* 2 | .rbx/* 3 | *.rbc 4 | *.lo 5 | *.la 6 | *.lai 7 | *.dylib.dSYM 8 | *.dylib 9 | *.o 10 | *.a 11 | *.log 12 | tmp/* 13 | true/* 14 | *.bundle 15 | *.gem 16 | doc/* 17 | .DS_Store 18 | ext/libjio 19 | pkg 20 | scratch 21 | ext/jio/dst 22 | ext/jio/Makefile -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | jio (0.1) 5 | 6 | GEM 7 | remote: http://rubygems.org/ 8 | specs: 9 | rake (0.9.2.2) 10 | rake-compiler (0.8.1) 11 | rake 12 | 13 | PLATFORMS 14 | ruby 15 | 16 | DEPENDENCIES 17 | jio! 18 | rake 19 | rake-compiler (~> 0.8.1) 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - rbx-18mode 4 | - rbx-19mode 5 | - ree 6 | - 1.8.7 7 | - 1.9.2 8 | - 1.9.3 9 | - ruby-head 10 | script: "bundle exec rake" 11 | gemfile: 12 | - Gemfile 13 | notifications: 14 | recipients: 15 | - lourens@methodmissing.com 16 | branches: 17 | only: 18 | - master -------------------------------------------------------------------------------- /ext/jio/ruby19.h: -------------------------------------------------------------------------------- 1 | #ifndef JIO_RUBY19_H 2 | #define JIO_RUBY19_H 3 | 4 | #include 5 | #include 6 | extern rb_encoding *binary_encoding; 7 | #define JioEncode(str) rb_enc_associate(str, binary_encoding) 8 | #ifndef THREAD_PASS 9 | #define THREAD_PASS rb_thread_schedule(); 10 | #endif 11 | 12 | #define TRAP_BEG 13 | #define TRAP_END 14 | 15 | #endif -------------------------------------------------------------------------------- /ext/jio/jruby.h: -------------------------------------------------------------------------------- 1 | #ifndef JIO_JRUBY_H 2 | #define JIO_JRUBY_H 3 | 4 | #include "st.h" 5 | 6 | /* XXX */ 7 | #define JioEncode(str) str 8 | #ifndef THREAD_PASS 9 | #define THREAD_PASS rb_thread_schedule(); 10 | #endif 11 | 12 | #define TRAP_BEG 13 | #define TRAP_END 14 | 15 | #undef rb_errinfo 16 | #define rb_errinfo() (rb_gv_get("$!")) 17 | 18 | #define HAVE_RB_THREAD_BLOCKING_REGION 1 19 | 20 | #endif -------------------------------------------------------------------------------- /ext/jio/jio_prelude.h: -------------------------------------------------------------------------------- 1 | #ifndef JIO_PRELUDE_H 2 | #define JIO_PRELUDE_H 3 | 4 | #ifndef RFLOAT_VALUE 5 | #define RFLOAT_VALUE(v) (RFLOAT(v)->value) 6 | #endif 7 | 8 | #ifdef RUBINIUS 9 | #include "rubinius.h" 10 | #else 11 | #ifdef JRUBY 12 | #include "jruby.h" 13 | #else 14 | #ifdef HAVE_RB_THREAD_BLOCKING_REGION 15 | #include "ruby19.h" 16 | #else 17 | #include "ruby18.h" 18 | #endif 19 | #endif 20 | #endif 21 | 22 | #endif -------------------------------------------------------------------------------- /ext/jio/rubinius.h: -------------------------------------------------------------------------------- 1 | #ifndef JIO_RUBINIUS_H 2 | #define JIO_RUBINIUS_H 3 | 4 | #define RSTRING_NOT_MODIFIED 5 | 6 | #ifdef HAVE_RUBY_ENCODING_H 7 | #include 8 | #include 9 | #include 10 | extern rb_encoding *binary_encoding; 11 | #define JioEncode(str) rb_enc_associate(str, binary_encoding) 12 | #else 13 | #include "st.h" 14 | #define JioEncode(str) str 15 | #endif 16 | 17 | #define TRAP_BEG 18 | #define TRAP_END 19 | 20 | #define THREAD_PASS rb_thread_schedule(); 21 | 22 | #endif -------------------------------------------------------------------------------- /ext/jio/file.h: -------------------------------------------------------------------------------- 1 | #ifndef JIO_FILE_H 2 | #define JIO_FILE_H 3 | 4 | #define JIO_FILE_CLOSED 0x01 5 | 6 | typedef struct { 7 | jfs_t *fs; 8 | int flags; 9 | } jio_jfs_wrapper; 10 | 11 | #define JioAssertFile(obj) JioAssertType(obj, rb_cJioFile, "JIO::File") 12 | #define JioGetFile(obj) \ 13 | jio_jfs_wrapper *file = NULL; \ 14 | JioAssertFile(obj); \ 15 | Data_Get_Struct(obj, jio_jfs_wrapper, file); \ 16 | if (!file) rb_raise(rb_eTypeError, "uninitialized JIO file handle!"); 17 | 18 | void _init_rb_jio_file(); 19 | 20 | #endif -------------------------------------------------------------------------------- /lib/jio/file.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module JIO 4 | class File 5 | alias orig_transaction transaction 6 | def transaction(flags) 7 | if block_given? 8 | begin 9 | trans = orig_transaction(flags) 10 | yield trans 11 | rescue 12 | trans.rollback 13 | error = true 14 | raise 15 | ensure 16 | trans.commit if !error && !trans.committed? 17 | trans.release 18 | end 19 | else 20 | orig_transaction(flags) 21 | end 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /test/test_jio.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require File.join(File.dirname(__FILE__), 'helper') 4 | 5 | class TestJio < JioTestCase 6 | def test_check 7 | file = JIO.open(*OPEN_ARGS) 8 | trans = file.transaction(JIO::J_LINGER) 9 | trans.write('COMMIT', 0) 10 | assert trans.commit 11 | assert trans.committed? 12 | expected = {:reapplied=>1, 13 | :invalid=>2, 14 | :corrupt=>0, 15 | :total=>3, 16 | :in_progress=>0, 17 | :broken=>0} 18 | assert_equal expected, JIO.check(FILE, 0) 19 | ensure 20 | trans.release 21 | assert file.close 22 | end 23 | end -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'test/unit' 4 | require 'jio' 5 | require 'fileutils' #19 6 | require 'timeout' 7 | 8 | TMP = File.expand_path(File.join(File.dirname(__FILE__), '..', 'tmp')) 9 | SANDBOX = File.join(TMP, 'sandbox') 10 | FileUtils.rm_rf SANDBOX 11 | FileUtils.mkdir_p SANDBOX 12 | 13 | class JioTestCase < Test::Unit::TestCase 14 | FILE = File.join(SANDBOX, 'file.jio') 15 | OPEN_ARGS = [FILE, JIO::RDWR | JIO::CREAT | JIO::TRUNC, 0644, JIO::J_LINGER] 16 | 17 | undef_method :default_test if method_defined? :default_test 18 | 19 | def setup 20 | File.unlink(FILE) if File.exists?(FILE) 21 | end 22 | 23 | if ENV['STRESS_GC'] 24 | def setup 25 | GC.stress = true 26 | end 27 | 28 | def teardown 29 | GC.stress = false 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /ext/jio/transaction.h: -------------------------------------------------------------------------------- 1 | #ifndef JIO_TRANSACTION_H 2 | #define JIO_TRANSACTION_H 3 | 4 | #define JIO_TRANSACTION_RELEASED 0x01 5 | 6 | typedef struct { 7 | jtrans_t *trans; 8 | VALUE views; 9 | int flags; 10 | } jio_jtrans_wrapper; 11 | 12 | #define JioAssertTransaction(obj) JioAssertType(obj, rb_cJioTransaction, "JIO::Transaction") 13 | #define JioGetTransaction(obj) \ 14 | jio_jtrans_wrapper *trans = NULL; \ 15 | JioAssertTransaction(obj); \ 16 | Data_Get_Struct(obj, jio_jtrans_wrapper, trans); \ 17 | if (!trans) rb_raise(rb_eTypeError, "uninitialized JIO transaction handle!"); 18 | 19 | void rb_jio_mark_transaction(void *ptr); 20 | void rb_jio_free_transaction(void *ptr); 21 | 22 | VALUE rb_jio_file_new_transaction(VALUE obj, VALUE flags); 23 | 24 | void _init_rb_jio_transaction(); 25 | 26 | #endif -------------------------------------------------------------------------------- /jio.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require File.expand_path('../lib/jio/version', __FILE__) 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "jio" 7 | s.version = JIO::VERSION 8 | s.summary = "jio - transactional, journaled file I/O for Ruby" 9 | s.description = "jio - transactional, journaled file I/O for Ruby" 10 | s.authors = ["Lourens Naudé"] 11 | s.email = ["lourens@methodmissing.com"] 12 | s.homepage = "http://github.com/methodmissing/jio" 13 | s.date = Time.now.utc.strftime('%Y-%m-%d') 14 | s.platform = Gem::Platform::RUBY 15 | s.extensions = Dir["ext/jio/extconf.rb"] 16 | s.has_rdoc = true 17 | s.files = `git ls-files`.split 18 | s.test_files = `git ls-files test`.split("\n") 19 | s.rdoc_options = ["--charset=UTF-8"] 20 | s.require_paths = ["lib"] 21 | s.add_development_dependency('rake-compiler', '~> 0.8.1') 22 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | jio is copyrighted Free Software by all contributors, see logs in 2 | revision control for names and email addresses of all of them. 3 | 4 | You can redistribute it and/or modify it under the terms of the GNU 5 | Lesser General Public License as published by the Free Software Foundation, 6 | version 2.1 or later {LGPLv2.1}[http://www.gnu.org/licenses/lgpl-2.1.txt] 7 | (see link:COPYING). 8 | 9 | eio is distributed in the hope that it will be useful, but WITHOUT 10 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 12 | License for more details. 13 | 14 | You should have received a copy of the GNU Lesser General Public License 15 | along with this library; if not, write to the Free Software 16 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 -------------------------------------------------------------------------------- /ext/jio/ruby18.h: -------------------------------------------------------------------------------- 1 | #ifndef JIO_RUBY18_H 2 | #define JIO_RUBY18_H 3 | 4 | #define THREAD_PASS rb_thread_schedule(); 5 | #define JioEncode(str) str 6 | #include "rubyio.h" 7 | #include "rubysig.h" 8 | #include "st.h" 9 | 10 | #ifndef RSTRING_PTR 11 | #define RSTRING_PTR(str) RSTRING(str)->ptr 12 | #endif 13 | #ifndef RSTRING_LEN 14 | #define RSTRING_LEN(s) (RSTRING(s)->len) 15 | #endif 16 | 17 | /* 18 | * partial emulation of the 1.9 rb_thread_blocking_region under 1.8, 19 | * this is enough for dealing with blocking I/O functions in the 20 | * presence of threads. 21 | */ 22 | 23 | #define RUBY_UBF_IO ((rb_unblock_function_t *)-1) 24 | typedef void rb_unblock_function_t(void *); 25 | typedef VALUE rb_blocking_function_t(void *); 26 | static VALUE 27 | rb_thread_blocking_region( 28 | rb_blocking_function_t *func, void *data1, 29 | JIO_UNUSED rb_unblock_function_t *ubf, 30 | JIO_UNUSED void *data2) 31 | { 32 | VALUE rv; 33 | TRAP_BEG; 34 | rv = func(data1); 35 | TRAP_END; 36 | return rv; 37 | } 38 | 39 | struct timeval rb_time_interval _((VALUE)); 40 | 41 | #define rb_errinfo() ruby_errinfo 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' unless defined?(Gem) 4 | require 'rake' unless defined?(Rake) 5 | 6 | require 'rake/extensiontask' 7 | require 'rake/testtask' 8 | begin 9 | require 'rdoc/task' 10 | rescue LoadError # fallback to older 1.8.7 rubies 11 | require 'rake/rdoctask' 12 | end 13 | 14 | gemspec = eval(IO.read('jio.gemspec')) 15 | 16 | Gem::PackageTask.new(gemspec) do |pkg| 17 | end 18 | 19 | Rake::ExtensionTask.new('jio', gemspec) do |ext| 20 | ext.name = 'jio_ext' 21 | ext.ext_dir = 'ext/jio' 22 | 23 | CLEAN.include 'ext/jio/dst' 24 | CLEAN.include 'ext/libjio' 25 | CLEAN.include 'lib/**/jio_ext.*' 26 | end 27 | 28 | Rake::RDocTask.new do |rd| 29 | files = FileList["README.rdoc", "lib/**/*.rb", "ext/jio/*.c"] 30 | rd.title = "jio - transactional, journaled file I/O for Ruby" 31 | rd.main = "README.rdoc" 32 | rd.rdoc_dir = "doc" 33 | rd.options << "--promiscuous" 34 | rd.rdoc_files.include(files) 35 | end 36 | 37 | desc 'Run jio tests' 38 | Rake::TestTask.new(:test) do |t| 39 | t.test_files = Dir.glob("test/**/test_*.rb") 40 | t.verbose = true 41 | t.warning = true 42 | end 43 | 44 | task :test => :compile 45 | task :default => :test 46 | -------------------------------------------------------------------------------- /ext/jio/jio_ext.h: -------------------------------------------------------------------------------- 1 | #ifndef JIO_EXT_H 2 | #define JIO_EXT_H 3 | 4 | #include "libjio.h" 5 | #include "trans.h" 6 | #include "ruby.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | /* Compiler specific */ 13 | 14 | #if defined(__GNUC__) && (__GNUC__ >= 3) 15 | #define JIO_UNUSED __attribute__ ((unused)) 16 | #define JIO_NOINLINE __attribute__ ((noinline)) 17 | #else 18 | #define JIO_UNUSED 19 | #define JIO_NOINLINE 20 | #endif 21 | 22 | #include "jio_prelude.h" 23 | 24 | #define JioAssertType(obj, type, desc) \ 25 | if (!rb_obj_is_kind_of(obj,type)) \ 26 | rb_raise(rb_eTypeError, "wrong argument type %s (expected %s): %s", rb_obj_classname(obj), desc, RSTRING_PTR(rb_obj_as_string(obj))); 27 | 28 | #define AssertOffset(off) \ 29 | Check_Type(off, T_FIXNUM); \ 30 | if (off < jio_zero) rb_raise(rb_eArgError, "offset must be >= 0"); \ 31 | 32 | #define AssertLength(len) \ 33 | Check_Type(len, T_FIXNUM); \ 34 | if (len < jio_zero) rb_raise(rb_eArgError, "length must be >= 0"); \ 35 | 36 | #include "file.h" 37 | #include "transaction.h" 38 | 39 | extern VALUE mJio; 40 | extern VALUE rb_cJioFile; 41 | extern VALUE rb_cJioTransaction; 42 | 43 | extern VALUE jio_zero; 44 | extern VALUE jio_empty_view; 45 | 46 | #endif -------------------------------------------------------------------------------- /test/test_file.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require File.join(File.dirname(__FILE__), 'helper') 4 | 5 | class TestFile < JioTestCase 6 | def test_open_close 7 | file = JIO.open(*OPEN_ARGS) 8 | assert_instance_of JIO::File, file 9 | ensure 10 | assert file.close 11 | end 12 | 13 | def test_sync 14 | file = JIO.open(*OPEN_ARGS) 15 | assert file.sync 16 | ensure 17 | assert file.close 18 | end 19 | 20 | def test_move_journal 21 | file = JIO.open(*OPEN_ARGS) 22 | assert file.move_journal(SANDBOX) 23 | ensure 24 | assert file.close 25 | end 26 | 27 | def test_autosync 28 | file = JIO.open(*OPEN_ARGS) 29 | assert file.autosync(1, 1024) 30 | file.write("CO") 31 | file.write("MM") 32 | file.write("IT") 33 | sleep 1.1 34 | file.rewind 35 | assert_equal "COMMIT", file.read(6) 36 | assert file.stop_autosync 37 | ensure 38 | assert file.close 39 | end 40 | 41 | def test_read_write 42 | file = JIO.open(*OPEN_ARGS) 43 | assert_equal "", file.read(0) 44 | assert_equal 4, file.write('ABCD') 45 | assert_equal 'ABCD', file.pread(4, 0) 46 | file.pwrite('EFGH', 4) 47 | assert_equal 'EFGH', file.pread(4, 4) 48 | file.pwrite('LMNOPQRS', 0) 49 | assert_equal 'LMNOPQRS', file.pread(8, 0) 50 | ensure 51 | assert file.close 52 | end 53 | 54 | def test_lseek 55 | file = JIO.open(*OPEN_ARGS) 56 | assert_equal 4, file.write('ABCD') 57 | file.lseek(2, IO::SEEK_SET) 58 | assert_equal 'CD', file.read(2) 59 | ensure 60 | assert file.close 61 | end 62 | 63 | def test_truncate 64 | file = JIO.open(*OPEN_ARGS) 65 | assert_equal 4, file.write('ABCD') 66 | assert_equal 4, File.size(FILE) 67 | file.truncate(2) 68 | assert_equal 2, File.size(FILE) 69 | file.truncate(0) 70 | assert_equal 0, File.size(FILE) 71 | ensure 72 | assert file.close 73 | end 74 | 75 | def test_fileno 76 | file = JIO.open(*OPEN_ARGS) 77 | assert_instance_of Fixnum, file.fileno 78 | ensure 79 | assert file.close 80 | end 81 | 82 | def test_rewind 83 | file = JIO.open(*OPEN_ARGS) 84 | file.write('ABCD') 85 | file.rewind 86 | assert_equal 'ABCD', file.read(4) 87 | ensure 88 | assert file.close 89 | end 90 | 91 | def test_tell 92 | file = JIO.open(*OPEN_ARGS) 93 | file.write('ABCD') 94 | assert_equal 4, file.tell 95 | ensure 96 | assert file.close 97 | end 98 | 99 | def test_eof_p 100 | file = JIO.open(*OPEN_ARGS) 101 | file.write('ABCD') 102 | assert file.eof? 103 | file.rewind 104 | assert !file.eof? 105 | ensure 106 | assert file.close 107 | end 108 | 109 | def test_error 110 | file = JIO.open(*OPEN_ARGS) 111 | file.write('ABCD') 112 | assert !file.error? 113 | ensure 114 | assert file.close 115 | end 116 | end -------------------------------------------------------------------------------- /test/test_transaction.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require File.join(File.dirname(__FILE__), 'helper') 4 | 5 | class TestTransaction < JioTestCase 6 | def test_spawn_transaction 7 | file = JIO.open(*OPEN_ARGS) 8 | trans = file.transaction(JIO::J_LINGER) 9 | assert_instance_of JIO::Transaction, trans 10 | ensure 11 | trans.release 12 | assert file.close 13 | end 14 | 15 | def test_spawn_transaction_with_block 16 | file = JIO.open(*OPEN_ARGS) 17 | file.transaction(JIO::J_LINGER) do |trans| 18 | assert_instance_of JIO::Transaction, trans 19 | trans.write("COMMIT", 0) 20 | end 21 | assert_equal "COMMIT", file.read(6) 22 | ensure 23 | assert file.close 24 | end 25 | 26 | def test_spawn_transaction_with_block_error 27 | file = JIO.open(*OPEN_ARGS) 28 | file.transaction(JIO::J_LINGER) do |trans| 29 | assert_instance_of JIO::Transaction, trans 30 | trans.write("COMMIT", 0) 31 | raise "rollback" 32 | end rescue nil 33 | assert_not_equal "COMMIT", file.read(6) 34 | ensure 35 | assert file.close 36 | end 37 | 38 | def test_transaction_views 39 | file = JIO.open(*OPEN_ARGS) 40 | trans = file.transaction(JIO::J_LINGER) 41 | trans.write('COMMIT', 0) 42 | trans.read(2, 2) 43 | assert_equal [], trans.views 44 | assert_equal nil, trans.release 45 | ensure 46 | assert file.close 47 | end 48 | 49 | def test_commit_transaction 50 | file = JIO.open(*OPEN_ARGS) 51 | trans = file.transaction(JIO::J_LINGER) 52 | trans.write('COMMIT', 0) 53 | trans.read(2, 2) 54 | trans.read(2, 4) 55 | assert_equal [], trans.views 56 | assert trans.commit 57 | assert_equal %w(MM IT), trans.views 58 | ensure 59 | trans.release 60 | file.close 61 | end 62 | 63 | def test_rollback_transaction 64 | file = JIO.open(*OPEN_ARGS) 65 | trans = file.transaction(JIO::J_LINGER) 66 | trans.write('COMMIT', 0) 67 | assert trans.commit 68 | assert trans.rollback 69 | ensure 70 | trans.release 71 | assert file.close 72 | end 73 | 74 | def test_transaction_committed_p 75 | file = JIO.open(*OPEN_ARGS) 76 | trans = file.transaction(JIO::J_LINGER) 77 | trans.write('COMMIT', 0) 78 | assert trans.commit 79 | assert trans.committed? 80 | ensure 81 | trans.release 82 | assert file.close 83 | end 84 | 85 | 86 | def test_transaction_rollbacked_p 87 | file = JIO.open(*OPEN_ARGS) 88 | trans = file.transaction(JIO::J_LINGER) 89 | trans.write('COMMIT', 0) 90 | assert trans.commit 91 | assert_equal 'COMMIT', file.read(6) 92 | assert trans.rollback 93 | # XXX: try to reproduce a rollbacked state 94 | assert !trans.rollbacked? 95 | ensure 96 | trans.release 97 | assert file.close 98 | end 99 | 100 | def test_transaction_rollbacking_p 101 | file = JIO.open(*OPEN_ARGS) 102 | trans = file.transaction(JIO::J_LINGER) 103 | trans.write('COMMIT', 0) 104 | assert trans.commit 105 | assert trans.rollback 106 | assert !trans.rollbacking? 107 | ensure 108 | trans.release 109 | assert file.close 110 | end 111 | end -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = jio - transactional, journaled file I/O for Ruby {Build Status}[http://travis-ci.org/methodmissing/jio] 2 | 3 | (c) 2011 Lourens Naudé (methodmissing), with API guidance from the libjio (http://blitiri.com.ar/p/libjio/) Python extension 4 | 5 | http://github.com/methodmissing/jio 6 | 7 | == Why you may need this 8 | 9 | Some problems don't map well to database systems and journaled flat files are often a good fit for append 10 | logs and Event Sourcing implementations. The API is simple (modeled to known UNIX and libc APIs), there's no external dependencies (we vendor libjio) and is known to work on most POSIX systems. The library (libjio) and thus this Ruby extension guarantees file integrity even after unexpected crashes, never leaving your files in an inconsistent state. Crash recovery is fast and safe as well. 11 | 12 | == How it works 13 | 14 | On the disk, the file you work on is exactly like a regular one, but a special directory is created to store in-flight transactions (lock file and transaction in contents). For further details see http://blitiri.com.ar/p/libjio/doc/libjio.html 15 | 16 | == Requirements 17 | 18 | * A POSIX compliant OS, known to work well on Linux, BSD variants and Mac OS X 19 | * Ruby MRI 1.8, 1.9 or Rubinius 20 | * A C compiler 21 | 22 | == Installation 23 | 24 | Rubygems installation 25 | 26 | gem install jio # pending upload to rubygems.org 27 | 28 | Building from source 29 | 30 | git clone git@github.com:methodmissing/jio.git 31 | rake 32 | 33 | Running tests 34 | 35 | rake test 36 | 37 | == Documentation 38 | 39 | RDOC document pending. 40 | 41 | == How to use 42 | 43 | # libjio file handle 44 | file = JIO.open("file.jio", JIO::RDWR | JIO::CREAT, 0600, JIO::J_LINGER) 45 | 46 | # spawn a new lingering transaction with a write operation 47 | trans = file.transaction(JIO::J_LINGER) 48 | trans.write('COMMIT', 0) 49 | 50 | # spawn 2 read operations 51 | trans.read(2, 2) 52 | trans.read(2, 4) 53 | 54 | # Empty views - not committed yet 55 | trans.views # [] 56 | # Commit write / read operations to / from disk 57 | trans.commit # true 58 | 59 | # View represents the 2 read operations requested earlier 60 | trans.views %w(MM IT) 61 | trans.committed? # true 62 | 63 | # rollback and cleanup 64 | trans.rollback 65 | trans.release 66 | file.close 67 | 68 | # Assert journal integrity 69 | JIO.check("file.jio", 0) # {"reapplied"=>1, 70 | # "invalid"=>0, 71 | # "corrupt"=>0, 72 | # "total"=>1, 73 | # "in_progress"=>0, 74 | # "broken"=>0} 75 | 76 | See the unit tests for further examples. 77 | 78 | == Todo 79 | 80 | * More intuitive API 81 | * Release the GIL where appropriate 82 | * Support vectored I/O readv and writev 83 | * More examples 84 | * Stress tests 85 | * Better test coverage 86 | * Gem release 87 | 88 | == Contact, feedback and bugs 89 | 90 | This project is still work in progress and I'm looking for guidance on API design, use cases and any outlier experiences. Please log bugs and suggestions at https://github.com/methodmissing/jio/issues 91 | 92 | Thanks ! -------------------------------------------------------------------------------- /ext/jio/extconf.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'mkmf' 4 | require 'pathname' 5 | 6 | def sys(cmd, err_msg) 7 | p cmd 8 | system(cmd) || fail(err_msg) 9 | end 10 | 11 | def fail(msg) 12 | STDERR.puts msg 13 | exit(1) 14 | end 15 | 16 | RbConfig::MAKEFILE_CONFIG['CC'] = ENV['CC'] if ENV['CC'] 17 | 18 | # XXX fallbacks specific to Darwin for JRuby (does not set these values in RbConfig::CONFIG) 19 | LIBEXT = RbConfig::CONFIG['LIBEXT'] || 'a' 20 | DLEXT = RbConfig::CONFIG['DLEXT'] || 'bundle' 21 | 22 | cwd = Pathname(File.expand_path(File.dirname(__FILE__))) 23 | dst_path = cwd + 'dst' 24 | libs_path = dst_path + 'lib' 25 | vendor_path = cwd + '..' 26 | libjio_path = vendor_path + 'libjio' 27 | libjio_include_path = libjio_path + 'libjio' 28 | 29 | # Courtesy of EventMachine and @tmm1 30 | def check_libs libs = [], fatal = false 31 | libs.all? { |lib| have_library(lib) || (abort("could not find library: #{lib}") if fatal) } 32 | end 33 | 34 | def check_heads heads = [], fatal = false 35 | heads.all? { |head| have_header(head) || (abort("could not find header: #{head}") if fatal)} 36 | end 37 | 38 | case RUBY_PLATFORM 39 | when /mswin32/, /mingw32/, /bccwin32/ 40 | check_heads(%w[windows.h winsock.h], true) 41 | check_libs(%w[kernel32 rpcrt4 gdi32], true) 42 | 43 | if GNU_CHAIN 44 | CONFIG['LDSHARED'] = "$(CXX) -shared -lstdc++" 45 | else 46 | $defs.push "-EHs" 47 | $defs.push "-GR" 48 | end 49 | 50 | when /solaris/ 51 | add_define 'OS_SOLARIS8' 52 | 53 | if CONFIG['CC'] == 'cc' and `cc -flags 2>&1` =~ /Sun/ # detect SUNWspro compiler 54 | # SUN CHAIN 55 | add_define 'CC_SUNWspro' 56 | $preload = ["\nCXX = CC"] # hack a CXX= line into the makefile 57 | $CFLAGS = CONFIG['CFLAGS'] = "-KPIC" 58 | CONFIG['CCDLFLAGS'] = "-KPIC" 59 | CONFIG['LDSHARED'] = "$(CXX) -G -KPIC -lCstd" 60 | else 61 | # GNU CHAIN 62 | # on Unix we need a g++ link, not gcc. 63 | CONFIG['LDSHARED'] = "$(CXX) -shared" 64 | end 65 | 66 | when /openbsd/ 67 | # OpenBSD branch contributed by Guillaume Sellier. 68 | 69 | # on Unix we need a g++ link, not gcc. On OpenBSD, linking against libstdc++ have to be explicitly done for shared libs 70 | CONFIG['LDSHARED'] = "$(CXX) -shared -lstdc++ -fPIC" 71 | CONFIG['LDSHAREDXX'] = "$(CXX) -shared -lstdc++ -fPIC" 72 | 73 | when /darwin/ 74 | # on Unix we need a g++ link, not gcc. 75 | # Ff line contributed by Daniel Harple. 76 | CONFIG['LDSHARED'] = "$(CXX) " + CONFIG['LDSHARED'].split[1..-1].join(' ') 77 | 78 | when /aix/ 79 | CONFIG['LDSHARED'] = "$(CXX) -shared -Wl,-G -Wl,-brtl" 80 | 81 | else 82 | # on Unix we need a g++ link, not gcc. 83 | CONFIG['LDSHARED'] = "$(CXX) -shared" 84 | end 85 | 86 | # extract dependencies 87 | unless File.directory?(libjio_path) 88 | fail "The 'tar' (creates and manipulates streaming archive files) utility is required to extract dependencies" if `which tar`.strip.empty? 89 | Dir.chdir(vendor_path) do 90 | sys "tar xvzf libjio.tar.gz", "Could not extract the libjio archive!" 91 | end 92 | end 93 | 94 | # build libjio 95 | lib = libs_path + "libjio.#{LIBEXT}" 96 | Dir.chdir libjio_path do 97 | sys "make DEBUG=1 PREFIX=#{dst_path} install", "libjio compile error!" 98 | end unless File.exist?(lib) 99 | 100 | dir_config('jio') 101 | 102 | have_func('rb_thread_blocking_region') 103 | 104 | $INCFLAGS << " -I#{libjio_include_path}" 105 | 106 | $LIBPATH << libs_path.to_s 107 | 108 | # Special case to prevent Rubinius compile from linking system libjio if present 109 | if defined?(RUBY_ENGINE) && RUBY_ENGINE =~ /rbx/ && RUBY_PLATFORM =~ /linux/ 110 | CONFIG['LDSHARED'] = "#{CONFIG['LDSHARED']} -Wl,-rpath=#{libs_path.to_s}" 111 | end 112 | 113 | fail "Error compiling and linking libjio" unless have_library("jio") 114 | 115 | $defs << "-pedantic" 116 | # libjio requires large file support 117 | $defs << "-D_LARGEFILE_SOURCE=1" 118 | $defs << "-D_LARGEFILE64_SOURCE=1" 119 | 120 | $CFLAGS << ' -Wall -funroll-loops' 121 | $CFLAGS << ' -Wextra -O0 -ggdb3' if ENV['DEBUG'] 122 | 123 | create_makefile('jio_ext') -------------------------------------------------------------------------------- /ext/jio/jio_ext.c: -------------------------------------------------------------------------------- 1 | #include "jio_ext.h" 2 | 3 | VALUE mJio; 4 | VALUE rb_cJioFile; 5 | VALUE rb_cJioTransaction; 6 | 7 | VALUE jio_zero; 8 | VALUE jio_empty_view; 9 | 10 | #ifdef HAVE_RUBY_ENCODING_H 11 | rb_encoding *binary_encoding; 12 | #endif 13 | 14 | static VALUE jio_s_total; 15 | static VALUE jio_s_invalid; 16 | static VALUE jio_s_in_progress; 17 | static VALUE jio_s_broken; 18 | static VALUE jio_s_corrupt; 19 | static VALUE jio_s_reapplied; 20 | 21 | /* 22 | * call-seq: 23 | * JIO.check("/path/file", JIO::J_CLEANUP) => Hash 24 | * 25 | * Checks and repairs a file previously created and managed through libjio. 26 | * 27 | * === Examples 28 | * JIO.check("/path/file", JIO::J_CLEANUP) => Hash 29 | * 30 | */ 31 | 32 | static VALUE rb_jio_s_check(JIO_UNUSED VALUE jio, VALUE path, VALUE flags) 33 | { 34 | int ret; 35 | VALUE result; 36 | struct jfsck_result res; 37 | Check_Type(path, T_STRING); 38 | Check_Type(flags, T_FIXNUM); 39 | ret = jfsck(RSTRING_PTR(path), NULL, &res, FIX2UINT(flags)); 40 | if (ret == J_ENOMEM) rb_memerror(); 41 | if (ret < 0) rb_sys_fail("jfsck"); 42 | result = rb_hash_new(); 43 | rb_hash_aset(result, jio_s_total, INT2NUM(res.total)); 44 | rb_hash_aset(result, jio_s_invalid, INT2NUM(res.invalid)); 45 | rb_hash_aset(result, jio_s_in_progress, INT2NUM(res.in_progress)); 46 | rb_hash_aset(result, jio_s_broken, INT2NUM(res.broken)); 47 | rb_hash_aset(result, jio_s_corrupt, INT2NUM(res.corrupt)); 48 | rb_hash_aset(result, jio_s_reapplied, INT2NUM(res.reapplied)); 49 | return result; 50 | } 51 | 52 | void 53 | Init_jio_ext() 54 | { 55 | mJio = rb_define_module("JIO"); 56 | 57 | /* 58 | * Generic globals (Fixnum 0 and empty Array for blank transaction views) 59 | */ 60 | jio_zero = INT2NUM(0); 61 | rb_gc_register_address(&jio_empty_view); 62 | jio_empty_view = rb_ary_new(); 63 | 64 | jio_s_total = ID2SYM(rb_intern("total")); 65 | jio_s_invalid = ID2SYM(rb_intern("invalid")); 66 | jio_s_in_progress = ID2SYM(rb_intern("in_progress")); 67 | jio_s_broken = ID2SYM(rb_intern("broken")); 68 | jio_s_corrupt = ID2SYM(rb_intern("corrupt")); 69 | jio_s_reapplied = ID2SYM(rb_intern("reapplied")); 70 | 71 | #ifdef HAVE_RUBY_ENCODING_H 72 | binary_encoding = rb_enc_find("binary"); 73 | #endif 74 | 75 | /* 76 | * Journal check specific constants 77 | */ 78 | rb_define_const(mJio, "J_ESUCCESS", INT2NUM(J_ESUCCESS)); 79 | rb_define_const(mJio, "J_ENOENT", INT2NUM(J_ENOENT)); 80 | rb_define_const(mJio, "J_ENOJOURNAL", INT2NUM(J_ENOJOURNAL)); 81 | rb_define_const(mJio, "J_ENOMEM", INT2NUM(J_ENOMEM)); 82 | rb_define_const(mJio, "J_ECLEANUP", INT2NUM(J_ECLEANUP)); 83 | rb_define_const(mJio, "J_EIO", INT2NUM(J_EIO)); 84 | 85 | /* 86 | * jfscheck specific constants 87 | */ 88 | rb_define_const(mJio, "J_CLEANUP", INT2NUM(J_CLEANUP)); 89 | 90 | /* 91 | * Open specific constants (POSIX) 92 | */ 93 | rb_define_const(mJio, "RDONLY", INT2NUM(O_RDONLY)); 94 | rb_define_const(mJio, "WRONLY", INT2NUM(O_WRONLY)); 95 | rb_define_const(mJio, "RDWR", INT2NUM(O_RDWR)); 96 | rb_define_const(mJio, "CREAT", INT2NUM(O_CREAT)); 97 | rb_define_const(mJio, "EXCL", INT2NUM(O_EXCL)); 98 | rb_define_const(mJio, "TRUNC", INT2NUM(O_TRUNC)); 99 | rb_define_const(mJio, "APPEND", INT2NUM(O_APPEND)); 100 | rb_define_const(mJio, "NONBLOCK", INT2NUM(O_NONBLOCK)); 101 | rb_define_const(mJio, "NDELAY", INT2NUM(O_NDELAY)); 102 | rb_define_const(mJio, "SYNC", INT2NUM(O_SYNC)); 103 | rb_define_const(mJio, "ASYNC", INT2NUM(O_ASYNC)); 104 | 105 | /* 106 | * lseek specific constants 107 | */ 108 | rb_define_const(mJio, "SEEK_SET", INT2NUM(SEEK_SET)); 109 | rb_define_const(mJio, "SEEK_CUR", INT2NUM(SEEK_CUR)); 110 | rb_define_const(mJio, "SEEK_END", INT2NUM(SEEK_END)); 111 | 112 | /* 113 | * JIO module methods 114 | */ 115 | rb_define_module_function(mJio, "check", rb_jio_s_check, 2); 116 | 117 | _init_rb_jio_file(); 118 | _init_rb_jio_transaction(); 119 | } -------------------------------------------------------------------------------- /ext/jio/transaction.c: -------------------------------------------------------------------------------- 1 | #include "jio_ext.h" 2 | 3 | /* 4 | * Generic transaction error handler 5 | */ 6 | static inline VALUE rb_jio_transaction_result(ssize_t ret, const char *ctx) 7 | { 8 | char err_buf[BUFSIZ]; 9 | if (ret >= 0) return Qtrue; 10 | if (ret == -1) snprintf(err_buf, BUFSIZ, "JIO transaction error on %s (atomic warranties preserved)", ctx); 11 | if (ret == -2) snprintf(err_buf, BUFSIZ, "JIO transaction error on %s (atomic warranties broken)", ctx); 12 | rb_sys_fail(err_buf); 13 | return INT2NUM(ret); 14 | } 15 | 16 | /* 17 | * GC callbacks for JIO::Transaction 18 | */ 19 | void rb_jio_mark_transaction(void *ptr) 20 | { 21 | jio_jtrans_wrapper *trans = (jio_jtrans_wrapper *)ptr; 22 | if (ptr) rb_gc_mark(trans->views); 23 | } 24 | 25 | void rb_jio_free_transaction(void *ptr) 26 | { 27 | jio_jtrans_wrapper *trans = (jio_jtrans_wrapper *)ptr; 28 | if(trans) { 29 | if (trans->trans != NULL && !(trans->flags & JIO_TRANSACTION_RELEASED)) jtrans_free(trans->trans); 30 | xfree(trans); 31 | } 32 | } 33 | 34 | /* 35 | * call-seq: 36 | * transaction.read(2, 2) => boolean 37 | * 38 | * Reads X bytes from Y offset into a transaction view. The buffered data is only visible once the 39 | * transaction has been committed. 40 | * 41 | * === Examples 42 | * transaction.read(2, 2) => boolean 43 | * 44 | */ 45 | 46 | static VALUE rb_jio_transaction_read(VALUE obj, VALUE length, VALUE offset) 47 | { 48 | int ret; 49 | VALUE buf; 50 | ssize_t len; 51 | JioGetTransaction(obj); 52 | AssertLength(length); 53 | AssertOffset(offset); 54 | len = (ssize_t)FIX2LONG(length); 55 | buf = rb_str_new(0, len); 56 | TRAP_BEG; 57 | ret = jtrans_add_r(trans->trans, RSTRING_PTR(buf), len, (off_t)NUM2OFFT(offset)); 58 | TRAP_END; 59 | if (ret == -1) rb_sys_fail("jtrans_add_r"); 60 | if (NIL_P(trans->views)) trans->views = rb_ary_new(); 61 | rb_ary_push(trans->views, JioEncode(buf)); 62 | return Qtrue; 63 | } 64 | 65 | /* 66 | * call-seq: 67 | * transaction.views => Array 68 | * 69 | * Returns a sequence of buffers representing previous transactional read operations. The result is 70 | * always an empty Array if the transaction hasn't been committed yet. Operations will be applied in 71 | * order, and overlapping operations are permitted, in which case the latest one will prevail. 72 | * 73 | * === Examples 74 | * transaction.views => Array 75 | * 76 | */ 77 | 78 | static VALUE rb_jio_transaction_views(VALUE obj) 79 | { 80 | jtrans_t *t = NULL; 81 | JioGetTransaction(obj); 82 | t = trans->trans; 83 | if ((t->flags & J_COMMITTED) && !NIL_P(trans->views)) return trans->views; 84 | return jio_empty_view; 85 | } 86 | 87 | /* 88 | * call-seq: 89 | * transaction.write("data", 2) => boolean 90 | * 91 | * Spawns a write operation from a given buffer to X offset for this transaction. Only written to disk 92 | * when the transaction has been committed. Operations will be applied in order, and overlapping 93 | * operations are permitted, in which case the latest one will prevail. 94 | * 95 | * === Examples 96 | * transaction.write("data", 2) => boolean 97 | * 98 | */ 99 | 100 | static VALUE rb_jio_transaction_write(VALUE obj, VALUE buf, VALUE offset) 101 | { 102 | int ret; 103 | JioGetTransaction(obj); 104 | Check_Type(buf, T_STRING); 105 | AssertOffset(offset); 106 | TRAP_BEG; 107 | ret = jtrans_add_w(trans->trans, RSTRING_PTR(buf), (size_t)RSTRING_LEN(buf), (off_t)NUM2OFFT(offset)); 108 | TRAP_END; 109 | if (ret == -1) rb_sys_fail("jtrans_add_w"); 110 | return Qtrue; 111 | } 112 | 113 | /* 114 | * call-seq: 115 | * transaction.commit => boolean 116 | * 117 | * Reads / writes all operations for this transaction to / from disk, in the order they were added. 118 | * After this function returns successfully, all the data can be trusted to be on the disk. The commit 119 | * is atomic with regards to other processes using libjio, but not accessing directly to the file. 120 | * 121 | * === Examples 122 | * transaction.commit => boolean 123 | * 124 | */ 125 | 126 | static VALUE rb_jio_transaction_commit(VALUE obj) 127 | { 128 | ssize_t ret; 129 | JioGetTransaction(obj); 130 | TRAP_BEG; 131 | ret = jtrans_commit(trans->trans); 132 | TRAP_END; 133 | return rb_jio_transaction_result(ret, "commit"); 134 | } 135 | 136 | /* 137 | * call-seq: 138 | * transaction.rollback => boolean 139 | * 140 | * This function atomically undoes a previous committed transaction. After its successful return, the 141 | * data can be trusted to be on disk. The read operations will be ignored as we only care about on disk 142 | * consistency. 143 | * 144 | * === Examples 145 | * transaction.rollback => boolean 146 | * 147 | */ 148 | 149 | static VALUE rb_jio_transaction_rollback(VALUE obj) 150 | { 151 | ssize_t ret; 152 | VALUE res; 153 | JioGetTransaction(obj); 154 | TRAP_BEG; 155 | ret = jtrans_rollback(trans->trans); 156 | TRAP_END; 157 | res = rb_jio_transaction_result(ret, "rollback"); 158 | if (!NIL_P(trans->views)) rb_ary_clear(trans->views); 159 | return res; 160 | } 161 | 162 | /* 163 | * call-seq: 164 | * transaction.release => nil 165 | * 166 | * Free all transaction state and operation buffers 167 | * 168 | * === Examples 169 | * transaction.release => nil 170 | * 171 | */ 172 | 173 | static VALUE rb_jio_transaction_release(VALUE obj) 174 | { 175 | JioGetTransaction(obj); 176 | TRAP_BEG; 177 | jtrans_free(trans->trans); 178 | TRAP_END; 179 | trans->flags |= JIO_TRANSACTION_RELEASED; 180 | return Qnil; 181 | } 182 | 183 | /* 184 | * call-seq: 185 | * transaction.committed? => boolean 186 | * 187 | * Determines if this transaction has been committed. 188 | * 189 | * === Examples 190 | * transaction.committed? => boolean 191 | * 192 | */ 193 | 194 | static VALUE rb_jio_transaction_committed_p(VALUE obj) 195 | { 196 | jtrans_t *t = NULL; 197 | JioGetTransaction(obj); 198 | t = trans->trans; 199 | return (t->flags & J_COMMITTED) ? Qtrue : Qfalse; 200 | } 201 | 202 | /* 203 | * call-seq: 204 | * transaction.rollbacked? => boolean 205 | * 206 | * Determines if this transaction has been rolled back. 207 | * 208 | * === Examples 209 | * transaction.rollbacked? => boolean 210 | * 211 | */ 212 | 213 | static VALUE rb_jio_transaction_rollbacked_p(VALUE obj) 214 | { 215 | jtrans_t *t = NULL; 216 | JioGetTransaction(obj); 217 | t = trans->trans; 218 | return (t->flags & J_ROLLBACKED) ? Qtrue : Qfalse; 219 | } 220 | 221 | /* 222 | * call-seq: 223 | * transaction.rollbacking? => boolean 224 | * 225 | * Determines if this transaction is in the process of being rolled back. 226 | * 227 | * === Examples 228 | * transaction.rollbacking? => boolean 229 | * 230 | */ 231 | 232 | static VALUE rb_jio_transaction_rollbacking_p(VALUE obj) 233 | { 234 | jtrans_t *t = NULL; 235 | JioGetTransaction(obj); 236 | t = trans->trans; 237 | return (t->flags & J_ROLLBACKING) ? Qtrue : Qfalse; 238 | } 239 | 240 | void _init_rb_jio_transaction() 241 | { 242 | rb_define_const(mJio, "J_NOLOCK", INT2NUM(J_NOLOCK)); 243 | rb_define_const(mJio, "J_NOROLLBACK", INT2NUM(J_NOROLLBACK)); 244 | rb_define_const(mJio, "J_LINGER", INT2NUM(J_LINGER)); 245 | rb_define_const(mJio, "J_COMMITTED", INT2NUM(J_COMMITTED)); 246 | rb_define_const(mJio, "J_ROLLBACKED", INT2NUM(J_ROLLBACKED)); 247 | rb_define_const(mJio, "J_ROLLBACKING", INT2NUM(J_ROLLBACKING)); 248 | rb_define_const(mJio, "J_RDONLY", INT2NUM(J_RDONLY)); 249 | 250 | rb_cJioTransaction = rb_define_class_under(mJio, "Transaction", rb_cObject); 251 | 252 | rb_define_method(rb_cJioTransaction, "read", rb_jio_transaction_read, 2); 253 | rb_define_method(rb_cJioTransaction, "views", rb_jio_transaction_views, 0); 254 | rb_define_method(rb_cJioTransaction, "write", rb_jio_transaction_write, 2); 255 | rb_define_method(rb_cJioTransaction, "commit", rb_jio_transaction_commit, 0); 256 | rb_define_method(rb_cJioTransaction, "rollback", rb_jio_transaction_rollback, 0); 257 | rb_define_method(rb_cJioTransaction, "release", rb_jio_transaction_release, 0); 258 | rb_define_method(rb_cJioTransaction, "committed?", rb_jio_transaction_committed_p, 0); 259 | rb_define_method(rb_cJioTransaction, "rollbacked?", rb_jio_transaction_rollbacked_p, 0); 260 | rb_define_method(rb_cJioTransaction, "rollbacking?", rb_jio_transaction_rollbacking_p, 0); 261 | } -------------------------------------------------------------------------------- /ext/jio/file.c: -------------------------------------------------------------------------------- 1 | #include "jio_ext.h" 2 | 3 | /* 4 | * GC callbacks for JIO::File 5 | */ 6 | static void rb_jio_free_file(void *ptr) 7 | { 8 | jio_jfs_wrapper *file = (jio_jfs_wrapper *)ptr; 9 | if (file) { 10 | if (file->fs != NULL && !(file->flags & JIO_FILE_CLOSED)) jclose(file->fs); 11 | xfree(file); 12 | } 13 | } 14 | 15 | /* 16 | * call-seq: 17 | * JIO.open("/path/file", JIO::CREAT | JIO::RDWR, 0600, JIO::J_LINGER) => JIO::File 18 | * 19 | * Returns a handle to a journaled file instance. Same semantics as the UNIX open(2) libc call, with 20 | * an additional one for libjio specific flags. 21 | * 22 | * === Examples 23 | * JIO.open("/path/file", JIO::CREAT | JIO::RDWR, 0600, JIO::J_LINGER) => JIO::File 24 | * 25 | */ 26 | 27 | static VALUE rb_jio_s_open(JIO_UNUSED VALUE jio, VALUE path, VALUE flags, VALUE mode, VALUE jflags) 28 | { 29 | VALUE obj; 30 | jio_jfs_wrapper *file = NULL; 31 | Check_Type(path, T_STRING); 32 | Check_Type(flags, T_FIXNUM); 33 | Check_Type(mode, T_FIXNUM); 34 | Check_Type(jflags, T_FIXNUM); 35 | obj = Data_Make_Struct(rb_cJioFile, jio_jfs_wrapper, 0, rb_jio_free_file, file); 36 | TRAP_BEG; 37 | file->fs = jopen(RSTRING_PTR(path), FIX2INT(flags), FIX2INT(mode), FIX2UINT(jflags)); 38 | TRAP_END; 39 | if (file->fs == NULL) { 40 | xfree(file); 41 | rb_sys_fail("jopen"); 42 | } 43 | file->flags = 0; 44 | rb_obj_call_init(obj, 0, NULL); 45 | return obj; 46 | } 47 | 48 | 49 | /* 50 | * call-seq: 51 | * file.sync => boolean 52 | * 53 | * Sync a file to disk. Makes sense only when using lingering transactions. 54 | * 55 | * === Examples 56 | * file.sync => boolean 57 | * 58 | */ 59 | 60 | static VALUE rb_jio_file_sync(VALUE obj) 61 | { 62 | JioGetFile(obj); 63 | TRAP_BEG; 64 | return (jsync(file->fs) == 0) ? Qtrue : Qfalse; 65 | TRAP_END; 66 | } 67 | 68 | /* 69 | * call-seq: 70 | * file.close => boolean 71 | * 72 | * After a call to this method, the memory allocated for the open file will be freed. If there was an 73 | * autosync thread started for this file, it will be stopped. 74 | * 75 | * === Examples 76 | * file.close => boolean 77 | * 78 | */ 79 | 80 | static VALUE rb_jio_file_close(VALUE obj) 81 | { 82 | JioGetFile(obj); 83 | TRAP_BEG; 84 | if (jclose(file->fs) != 0) return Qfalse; 85 | TRAP_END; 86 | file->flags |= JIO_FILE_CLOSED; 87 | return Qtrue; 88 | } 89 | 90 | /* 91 | * call-seq: 92 | * file.move_journal("/path") => boolean 93 | * 94 | * Changes the location of the journal direction. The file cannot be in use at this time. 95 | * 96 | * === Examples 97 | * file.move_journal("/path") => boolean 98 | * 99 | */ 100 | 101 | static VALUE rb_jio_file_move_journal(VALUE obj, VALUE path) 102 | { 103 | JioGetFile(obj); 104 | Check_Type(path, T_STRING); 105 | TRAP_BEG; 106 | return (jmove_journal(file->fs, RSTRING_PTR(path)) == 0) ? Qtrue : Qfalse; 107 | TRAP_END; 108 | } 109 | 110 | /* 111 | * call-seq: 112 | * file.autosync(5, 4000) => boolean 113 | * 114 | * Syncs to disk every X seconds, or every Y bytes written. Only one autosync thread per open file is 115 | * allowed. Only makes sense with lingering transactions. 116 | * 117 | * === Examples 118 | * file.autosync(5, 4000) => boolean 119 | * 120 | */ 121 | 122 | static VALUE rb_jio_file_autosync(VALUE obj, VALUE max_seconds, VALUE max_bytes) 123 | { 124 | JioGetFile(obj); 125 | Check_Type(max_seconds, T_FIXNUM); 126 | Check_Type(max_bytes, T_FIXNUM); 127 | TRAP_BEG; 128 | return (jfs_autosync_start(file->fs, (time_t)FIX2LONG(max_seconds), (size_t)FIX2LONG(max_bytes)) == 0) ? Qtrue : Qfalse; 129 | TRAP_END; 130 | } 131 | 132 | /* 133 | * call-seq: 134 | * file.stop_autosync => boolean 135 | * 136 | * Stops a previously started autosync thread. 137 | * 138 | * === Examples 139 | * file.stop_autosync => boolean 140 | * 141 | */ 142 | 143 | static VALUE rb_jio_file_stop_autosync(VALUE obj) 144 | { 145 | JioGetFile(obj); 146 | TRAP_BEG; 147 | return (jfs_autosync_stop(file->fs) == 0) ? Qtrue : Qfalse; 148 | TRAP_END; 149 | } 150 | 151 | /* 152 | * call-seq: 153 | * file.read(10) => String 154 | * 155 | * Reads from a libjio file handle. Works just like UNIX read(2) 156 | * 157 | * === Examples 158 | * file.read(10) => String 159 | * 160 | */ 161 | 162 | static VALUE rb_jio_file_read(VALUE obj, VALUE length) 163 | { 164 | ssize_t bytes; 165 | char *buf = NULL; 166 | ssize_t len; 167 | JioGetFile(obj); 168 | AssertLength(length); 169 | len = (ssize_t)FIX2LONG(length); 170 | buf = xmalloc(len + 1); 171 | if (buf == NULL) rb_memerror(); 172 | TRAP_BEG; 173 | bytes = jread(file->fs, buf, len); 174 | TRAP_END; 175 | if (bytes == -1) { 176 | xfree(buf); 177 | rb_sys_fail("jread"); 178 | } 179 | return JioEncode(rb_str_new(buf, (long)len)); 180 | } 181 | 182 | /* 183 | * call-seq: 184 | * file.pread(10, 10) => String 185 | * 186 | * Reads from a libjio file handle at a given offset. Works just like UNIX pread(2) 187 | * 188 | * === Examples 189 | * file.pread(10, 10) => String 190 | * 191 | */ 192 | 193 | static VALUE rb_jio_file_pread(VALUE obj, VALUE length, VALUE offset) 194 | { 195 | ssize_t bytes; 196 | char *buf = NULL; 197 | ssize_t len; 198 | JioGetFile(obj); 199 | AssertLength(length); 200 | AssertOffset(offset); 201 | len = (ssize_t)FIX2LONG(length); 202 | buf = xmalloc(len + 1); 203 | if (buf == NULL) rb_memerror(); 204 | TRAP_BEG; 205 | bytes = jpread(file->fs, buf, len, (off_t)NUM2OFFT(offset)); 206 | TRAP_END; 207 | if (bytes == -1) { 208 | xfree(buf); 209 | rb_sys_fail("jpread"); 210 | } 211 | return JioEncode(rb_str_new(buf, (long)len)); 212 | } 213 | 214 | /* 215 | * call-seq: 216 | * file.write("buffer") => Fixnum 217 | * 218 | * Writes to a libjio file handle. Works just like UNIX write(2) 219 | * 220 | * === Examples 221 | * file.write("buffer") => Fixnum 222 | * 223 | */ 224 | 225 | static VALUE rb_jio_file_write(VALUE obj, VALUE buf) 226 | { 227 | ssize_t bytes; 228 | JioGetFile(obj); 229 | Check_Type(buf, T_STRING); 230 | TRAP_BEG; 231 | bytes = jwrite(file->fs, RSTRING_PTR(buf), (size_t)RSTRING_LEN(buf)); 232 | TRAP_END; 233 | if (bytes == -1) rb_sys_fail("jwrite"); 234 | return INT2NUM(bytes); 235 | } 236 | 237 | /* 238 | * call-seq: 239 | * file.pwrite("buffer", 10) => Fixnum 240 | * 241 | * Writes to a libjio file handle at a given offset. Works just like UNIX pwrite(2) 242 | * 243 | * === Examples 244 | * file.pwrite("buffer", 10) => Fixnum 245 | * 246 | */ 247 | 248 | static VALUE rb_jio_file_pwrite(VALUE obj, VALUE buf, VALUE offset) 249 | { 250 | ssize_t bytes; 251 | JioGetFile(obj); 252 | Check_Type(buf, T_STRING); 253 | AssertOffset(offset); 254 | TRAP_BEG; 255 | bytes = jpwrite(file->fs, RSTRING_PTR(buf), (size_t)RSTRING_LEN(buf), (off_t)NUM2OFFT(offset)); 256 | TRAP_END; 257 | if (bytes == -1) rb_sys_fail("jpwrite"); 258 | return INT2NUM(bytes); 259 | } 260 | 261 | /* 262 | * call-seq: 263 | * file.lseek(10, JIO::SEEK_SET) => Fixnum 264 | * 265 | * Reposition the file pointer to the given offset, according to the whence directive. 266 | * 267 | * === Examples 268 | * file.lseek(10, JIO::SEEK_SET) => Fixnum 269 | * 270 | */ 271 | 272 | static VALUE rb_jio_file_lseek(VALUE obj, VALUE offset, VALUE whence) 273 | { 274 | off_t off; 275 | JioGetFile(obj); 276 | AssertOffset(offset); 277 | Check_Type(whence, T_FIXNUM); 278 | TRAP_BEG; 279 | off = jlseek(file->fs, (off_t)NUM2OFFT(offset), FIX2INT(whence)); 280 | TRAP_END; 281 | if (off == -1) rb_sys_fail("jlseek"); 282 | return OFFT2NUM(off); 283 | } 284 | 285 | /* 286 | * call-seq: 287 | * file.truncate(10) => Fixnum 288 | * 289 | * Truncate the file to the given size. 290 | * 291 | * === Examples 292 | * file.truncate(10) => Fixnum 293 | * 294 | */ 295 | 296 | static VALUE rb_jio_file_truncate(VALUE obj, VALUE length) 297 | { 298 | off_t len; 299 | JioGetFile(obj); 300 | AssertLength(length); 301 | TRAP_BEG; 302 | len = jtruncate(file->fs, (off_t)NUM2OFFT(length)); 303 | TRAP_END; 304 | if (len == -1) rb_sys_fail("jtruncate"); 305 | return OFFT2NUM(len); 306 | } 307 | 308 | /* 309 | * call-seq: 310 | * file.fileno => Fixnum 311 | * 312 | * Return the file descriptor number for the file. 313 | * 314 | * === Examples 315 | * file.fileno => Fixnum 316 | * 317 | */ 318 | 319 | static VALUE rb_jio_file_fileno(VALUE obj) 320 | { 321 | int fd; 322 | JioGetFile(obj); 323 | TRAP_BEG; 324 | fd = jfileno(file->fs); 325 | TRAP_END; 326 | if (fd == -1) rb_sys_fail("jfileno"); 327 | return INT2NUM(fd); 328 | } 329 | 330 | /* 331 | * call-seq: 332 | * file.rewind => nil 333 | * 334 | * Adjusts the file so that the next I/O operation will take place at the beginning of the file. 335 | * 336 | * === Examples 337 | * file.rewind => nil 338 | * 339 | */ 340 | 341 | static VALUE rb_jio_file_rewind(VALUE obj) 342 | { 343 | JioGetFile(obj); 344 | TRAP_BEG; 345 | jrewind(file->fs); 346 | TRAP_END; 347 | return Qnil; 348 | } 349 | 350 | /* 351 | * call-seq: 352 | * file.tell => Fixnum 353 | * 354 | * Determine the current file offset. 355 | * 356 | * === Examples 357 | * file.tell => Fixnum 358 | * 359 | */ 360 | 361 | static VALUE rb_jio_file_tell(VALUE obj) 362 | { 363 | long size; 364 | JioGetFile(obj); 365 | TRAP_BEG; 366 | size = jftell(file->fs); 367 | TRAP_END; 368 | if (size == -1) rb_sys_fail("jftell"); 369 | return INT2NUM(size); 370 | } 371 | 372 | /* 373 | * call-seq: 374 | * file.eof? => boolean 375 | * 376 | * Check for end-of-file. 377 | * 378 | * === Examples 379 | * file.eof? => boolean 380 | * 381 | */ 382 | 383 | static VALUE rb_jio_file_eof_p(VALUE obj) 384 | { 385 | JioGetFile(obj); 386 | TRAP_BEG; 387 | return (jfeof(file->fs) != 0) ? Qtrue : Qfalse; 388 | TRAP_END; 389 | } 390 | 391 | /* 392 | * call-seq: 393 | * file.error? => boolean 394 | * 395 | * Determines if an error condition has occurred. 396 | * 397 | * === Examples 398 | * file.error? => boolean 399 | * 400 | */ 401 | 402 | static VALUE rb_jio_file_error_p(VALUE obj) 403 | { 404 | int res; 405 | JioGetFile(obj); 406 | TRAP_BEG; 407 | res = jferror(file->fs); 408 | TRAP_END; 409 | if (res == 0) return Qfalse; 410 | return INT2NUM(res); 411 | } 412 | 413 | /* 414 | * call-seq: 415 | * file.clearerr => nil 416 | * 417 | * Resets the error flag for this file, if any. 418 | * 419 | * === Examples 420 | * file.clearerr => nil 421 | * 422 | */ 423 | 424 | static VALUE rb_jio_file_clearerr(VALUE obj) 425 | { 426 | JioGetFile(obj); 427 | TRAP_BEG; 428 | jclearerr(file->fs); 429 | TRAP_END; 430 | return Qnil; 431 | } 432 | 433 | /* 434 | * call-seq: 435 | * file.transaction(JIO::J_LINGER) => JIO::Transaction 436 | * 437 | * Creates a new low level transaction from a libjio file reference. 438 | * 439 | * === Examples 440 | * file.transaction(JIO::J_LINGER) => JIO::Transaction 441 | * 442 | */ 443 | 444 | VALUE rb_jio_file_new_transaction(VALUE obj, VALUE flags) 445 | { 446 | VALUE transaction; 447 | jio_jtrans_wrapper *trans = NULL; 448 | JioGetFile(obj); 449 | Check_Type(flags, T_FIXNUM); 450 | transaction = Data_Make_Struct(rb_cJioTransaction, jio_jtrans_wrapper, rb_jio_mark_transaction, rb_jio_free_transaction, trans); 451 | TRAP_BEG; 452 | trans->trans = jtrans_new(file->fs, FIX2INT(flags)); 453 | TRAP_END; 454 | if (trans->trans == NULL) { 455 | xfree(trans); 456 | rb_sys_fail("jtrans_new"); 457 | } 458 | trans->views = Qnil; 459 | trans->flags = 0; 460 | rb_obj_call_init(transaction, 0, NULL); 461 | return transaction; 462 | } 463 | 464 | void _init_rb_jio_file() 465 | { 466 | rb_define_module_function(mJio, "open", rb_jio_s_open, 4); 467 | 468 | rb_cJioFile = rb_define_class_under(mJio, "File", rb_cObject); 469 | 470 | rb_define_method(rb_cJioFile, "sync", rb_jio_file_sync, 0); 471 | rb_define_method(rb_cJioFile, "close", rb_jio_file_close, 0); 472 | rb_define_method(rb_cJioFile, "move_journal", rb_jio_file_move_journal, 1); 473 | rb_define_method(rb_cJioFile, "autosync", rb_jio_file_autosync, 2); 474 | rb_define_method(rb_cJioFile, "stop_autosync", rb_jio_file_stop_autosync, 0); 475 | rb_define_method(rb_cJioFile, "read", rb_jio_file_read, 1); 476 | rb_define_method(rb_cJioFile, "pread", rb_jio_file_pread, 2); 477 | rb_define_method(rb_cJioFile, "write", rb_jio_file_write, 1); 478 | rb_define_method(rb_cJioFile, "pwrite", rb_jio_file_pwrite, 2); 479 | rb_define_method(rb_cJioFile, "lseek", rb_jio_file_lseek, 2); 480 | rb_define_method(rb_cJioFile, "truncate", rb_jio_file_truncate, 1); 481 | rb_define_method(rb_cJioFile, "fileno", rb_jio_file_fileno, 0); 482 | rb_define_method(rb_cJioFile, "rewind", rb_jio_file_rewind, 0); 483 | rb_define_method(rb_cJioFile, "tell", rb_jio_file_tell, 0); 484 | rb_define_method(rb_cJioFile, "eof?", rb_jio_file_eof_p, 0); 485 | rb_define_method(rb_cJioFile, "error?", rb_jio_file_error_p, 0); 486 | rb_define_method(rb_cJioFile, "clearerr", rb_jio_file_clearerr, 0); 487 | rb_define_method(rb_cJioFile, "transaction", rb_jio_file_new_transaction, 1); 488 | } --------------------------------------------------------------------------------