cassandra_archive 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ .idea/
2
+ *.gem
3
+ *.rbc
4
+ .yardoc
5
+ _yardoc
6
+ doc/
7
+ pkg
8
+ rdoc
9
+ test/tmp
10
+ test/version_tmp
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'activesupport', '~> 3'
4
+ gem 'activemodel', '~> 3'
5
+ gem 'activerecord', '~> 3'
6
+ gem 'exception_helper'
7
+
8
+ gem "cassandra", :require => 'cassandra/1.2'
9
+ gem 'active_attr', :git => "http://github.com/backupify/active_attr.git"
10
+
11
+ # Add dependencies to develop your gem here.
12
+ # Include everything needed to run rake, tests, features, etc.
13
+ group :development do
14
+ gem "shoulda"
15
+ gem "factory_girl"
16
+ gem "bundler"
17
+ gem 'mocha'
18
+ gem 'sqlite3'
19
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,67 @@
1
+ GIT
2
+ remote: http://github.com/backupify/active_attr.git
3
+ revision: 69951a36e62bc348b6d2c86ce50c1251ad709e34
4
+ specs:
5
+ active_attr (0.8.2)
6
+ activemodel (>= 3.0.2, < 4.1)
7
+ activesupport (>= 3.0.2, < 4.1)
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ activemodel (3.2.14)
13
+ activesupport (= 3.2.14)
14
+ builder (~> 3.0.0)
15
+ activerecord (3.2.14)
16
+ activemodel (= 3.2.14)
17
+ activesupport (= 3.2.14)
18
+ arel (~> 3.0.2)
19
+ tzinfo (~> 0.3.29)
20
+ activesupport (3.2.14)
21
+ i18n (~> 0.6, >= 0.6.4)
22
+ multi_json (~> 1.0)
23
+ arel (3.0.2)
24
+ builder (3.0.4)
25
+ cassandra (0.22.0)
26
+ json
27
+ rake
28
+ simple_uuid (~> 0.2.0)
29
+ thrift_client (~> 0.7, < 0.9)
30
+ exception_helper (0.1.2)
31
+ factory_girl (4.2.0)
32
+ activesupport (>= 3.0.0)
33
+ i18n (0.6.5)
34
+ json (1.8.0)
35
+ metaclass (0.0.1)
36
+ mocha (0.14.0)
37
+ metaclass (~> 0.0.1)
38
+ multi_json (1.7.9)
39
+ rake (10.1.0)
40
+ shoulda (3.5.0)
41
+ shoulda-context (~> 1.0, >= 1.0.1)
42
+ shoulda-matchers (>= 1.4.1, < 3.0)
43
+ shoulda-context (1.1.5)
44
+ shoulda-matchers (2.3.0)
45
+ activesupport (>= 3.0.0)
46
+ simple_uuid (0.2.0)
47
+ sqlite3 (1.3.8)
48
+ thrift (0.8.0)
49
+ thrift_client (0.8.4)
50
+ thrift (~> 0.8.0)
51
+ tzinfo (0.3.37)
52
+
53
+ PLATFORMS
54
+ ruby
55
+
56
+ DEPENDENCIES
57
+ active_attr!
58
+ activemodel (~> 3)
59
+ activerecord (~> 3)
60
+ activesupport (~> 3)
61
+ bundler
62
+ cassandra
63
+ exception_helper
64
+ factory_girl
65
+ mocha
66
+ shoulda
67
+ sqlite3
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Backupify
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # Description
2
+
3
+ The active record extension implements the archive functionality to cassandra database. After record get deleted it replicates the copy of all attributes of that object to cassandra.
4
+
5
+ # Example of usage
6
+
7
+ Before you start using library you need to create column family where archived records will be stored
8
+
9
+ create keyspace CassandraArchive_test;
10
+ create column family DeletedRecords with column_type='Super' and comparator='UTF8Type' and subcomparator='UTF8Type';
11
+
12
+ initialize cassandra connection
13
+
14
+ ::CASSANDRA_CLIENT = Cassandra.new(keyspace, hosts)
15
+
16
+ and include module into model you want to archive after destroying
17
+
18
+ class Service < ActiveRecord::Base
19
+ include CassandraArchive
20
+ end
21
+
22
+ After you delete a record, it will be automatically replicated to cassandra in the following format
23
+
24
+ column_family will be 'DeletedRecords'
25
+ row_id will be the same as table name, for example above it will be 'services'
26
+ column_name is the removal timestamp, it looks like '1361974054666398'
27
+ column_attributes will contain the active record attributes
28
+
29
+ The model will be extended with :archived method
30
+
31
+ Service.archived # returns list of all archived records for that model
32
+ Service.archived(:after => 3.day.ago) # returns list of archived records for last 3 days
33
+
34
+ Service.archived do |timestamp, attributes|
35
+ # the block is called for each archived record
36
+ # timestamp shows when record has been archived
37
+ end
38
+
39
+ You may want to store the data which is not presented as active record attribute, it can be dynamic attribute or something like that. For that purpose you define :cassandra_archive_attributes method where you define the list of attributes you want to archive.
40
+
41
+ class Service < ActiveRecord::Base
42
+ include CassandraArchive
43
+
44
+ def cassandra_archive_attributes
45
+ # all active record attributes will be archived plus :account_name
46
+ attributes.keys + [:account_name]
47
+ end
48
+
49
+ def account_name
50
+ # the code here does request to the service in order to get account name
51
+ end
52
+ end
53
+
54
+ ## Running tests
55
+
56
+ Before you run tests do this in cassandra-cli:
57
+
58
+ use CassandraArchive_test; # create it first if it doesn't exist
59
+ create column family DeletedRecords with column_type='Super' and comparator='UTF8Type' and subcomparator='UTF8Type';
60
+
61
+ then run rake
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.pattern = 'test/**/*_test.rb'
8
+ test.verbose = true
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cassandra_archive/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "cassandra_archive"
8
+ gem.version = CassandraArchive::VERSION
9
+ gem.authors = ["Alexander Litvinovsky","Jason Haruska"]
10
+ gem.email = ["dev@backupify.com"]
11
+ gem.description = "The library allows to archive destroyed record to cassandra. For that moment ActiveRecord is supported."
12
+ gem.summary = "Archiving and retrieving records to cassandra"
13
+ gem.homepage = "http://github.com/backupify/cassandra_archive"
14
+ gem.license = "MIT"
15
+
16
+ gem.add_runtime_dependency("activerecord", [">= 3.0.0"])
17
+ gem.add_runtime_dependency("cassandra", [">= 0.12.2"])
18
+
19
+ gem.add_development_dependency('sqlite3')
20
+ gem.add_development_dependency('shoulda')
21
+ gem.add_development_dependency('mocha')
22
+
23
+ gem.files = `git ls-files`.split($/)
24
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
25
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
26
+ gem.require_paths = ["lib"]
27
+ end
@@ -0,0 +1,62 @@
1
+ require 'cassandra'
2
+ require 'active_support/concern'
3
+
4
+ require 'cassandra_archive/version'
5
+ require 'cassandra_archive/helper'
6
+
7
+ module CassandraArchive
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ after_commit :on => :destroy do
12
+ archive
13
+ end
14
+ end
15
+
16
+ def archive
17
+ time = DateTime.current
18
+
19
+ cassandra_timestamp = Helper.timestamp(time)
20
+ unix_timestamp = time.to_i.to_s
21
+
22
+ cassandra_attributes = archived_attributes.merge('archived_at' => unix_timestamp)
23
+ ::CASSANDRA_CLIENT.insert('DeletedRecords', self.class.table_name, {cassandra_timestamp.to_s => cassandra_attributes})
24
+ end
25
+
26
+ def cassandra_archive_attributes
27
+ # return active record attributes by default
28
+ attributes.keys
29
+ end
30
+
31
+ def archived_attributes
32
+ cassandra_archive_attributes.inject({}) do |hash, attribute|
33
+ value = send(attribute).to_s
34
+ cassandra_encoded_value = Helper.encode_for_cassandra(value)
35
+ hash[attribute.to_s] = cassandra_encoded_value
36
+ hash
37
+ end
38
+ end
39
+
40
+ module ClassMethods
41
+ def archived(options = {})
42
+ if time = options.delete(:after)
43
+ options[:start] = Helper.timestamp(time).to_s
44
+ end
45
+
46
+ records = ::CASSANDRA_CLIENT.get('DeletedRecords', table_name, options)
47
+
48
+ # encode attributes to utf8
49
+ records.each_entry do |entry|
50
+ entry.last.keys.each do |key|
51
+ entry.last[key].force_encoding('UTF-8')
52
+ end
53
+ end
54
+
55
+ if block_given?
56
+ records.each {|key, value| yield key, value}
57
+ end
58
+
59
+ records
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,17 @@
1
+ module CassandraArchive
2
+ module Helper
3
+ def self.timestamp(time)
4
+ (time.to_f * 1_000_000).to_i
5
+ end
6
+
7
+ def self.encode_for_cassandra(str, opts = {})
8
+ encode_opts = {
9
+ :invalid => :replace,
10
+ :undef => :replace,
11
+ :replace => ''
12
+ }.merge(opts)
13
+
14
+ str.encode('UTF-8', encode_opts).force_encoding('ASCII-8BIT')
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ module CassandraArchive
2
+ VERSION = '0.2.0'
3
+ end
4
+
data/test/schema.rb ADDED
@@ -0,0 +1,7 @@
1
+ ActiveRecord::Schema.define(:version => 1) do
2
+ create_table :test_models, :force => true do |t|
3
+ t.column :firstname, :string
4
+ t.column :lastname, :string
5
+ t.column :created_at, :timestamp
6
+ end
7
+ end
@@ -0,0 +1,30 @@
1
+ require 'test/unit'
2
+ require 'mocha/setup'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+
8
+ require 'cassandra_archive'
9
+
10
+ ::CASSANDRA_CLIENT = Cassandra.new('CassandraArchive_test', %w[localhost:9160])
11
+
12
+ class Test::Unit::TestCase
13
+ def setup
14
+ ::CASSANDRA_CLIENT.clear_keyspace!
15
+ end
16
+ end
17
+
18
+ require 'active_record'
19
+
20
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
21
+ load(File.dirname(__FILE__) + "/schema.rb")
22
+
23
+ # enable coverage reports for jenkins only
24
+ if ENV['CI']
25
+ puts "Enabling simplecov(rcov) for jenkins"
26
+ require 'simplecov'
27
+ require 'simplecov-rcov'
28
+ SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
29
+ SimpleCov.start
30
+ end
@@ -0,0 +1,105 @@
1
+ # coding: utf-8
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper.rb')
4
+
5
+ class TestModel < ActiveRecord::Base
6
+ include ::CassandraArchive
7
+
8
+ def cassandra_archive_attributes
9
+ [:id, :firstname, :lastname, :fullname]
10
+ end
11
+
12
+ def fullname
13
+ "#{firstname} #{lastname}"
14
+ end
15
+ end
16
+
17
+ class CassandraArchiveTest < Test::Unit::TestCase
18
+ context 'archived' do
19
+ setup do
20
+ @record = TestModel.create(:firstname => "firstname", :lastname => "lastname")
21
+ @record.destroy
22
+ end
23
+
24
+ should 'archive record to cassandra after record destroying' do
25
+ archived_record = find_archived_record(@record)
26
+
27
+ assert_equal @record.id.to_s, archived_record['id']
28
+ assert_equal @record.firstname, archived_record['firstname']
29
+ assert_equal @record.lastname, archived_record['lastname']
30
+ assert_equal @record.fullname, archived_record['fullname']
31
+
32
+ # we didn't list created_at attribute in cassandra_archive_attributes method, so it should not be archived
33
+ assert_equal nil, archived_record['created_at']
34
+ end
35
+
36
+ should 'archive record if attributes contain not ASCII chars' do
37
+ @record = TestModel.create(:firstname => "Иван", :lastname => "Иванов")
38
+ @record.destroy
39
+
40
+ archived_record = find_archived_record(@record)
41
+
42
+ assert_equal @record.id.to_s, archived_record['id']
43
+ assert_equal @record.firstname, archived_record['firstname']
44
+ assert_equal @record.lastname, archived_record['lastname']
45
+ assert_equal @record.fullname, archived_record['fullname']
46
+
47
+ # we didn't list created_at attribute in cassandra_archive_attributes method, so it should not be archived
48
+ assert_equal nil, archived_record['created_at']
49
+ end
50
+
51
+ should 'set archived_at attribute' do
52
+ time = DateTime.current
53
+ DateTime.stubs(:current).returns(time)
54
+
55
+ archived_at = time.to_i.to_s
56
+
57
+ record = TestModel.create(:firstname => "firstname", :lastname => "lastname")
58
+ record.destroy
59
+
60
+ archived_record = find_archived_record(@record)
61
+ assert_equal archived_at, archived_record['archived_at']
62
+ end
63
+
64
+ should 'return the number of archived records' do
65
+ assert_equal 1, TestModel.archived.size
66
+
67
+ another_record = TestModel.create(:firstname => "firstname", :lastname => "lastname")
68
+ another_record.destroy
69
+
70
+ assert_equal 2, TestModel.archived.size
71
+ end
72
+
73
+ should 'return records archived after specified date if :after option is passed as timestamp' do
74
+ assert_equal 1, TestModel.archived(:after => @record.created_at.to_i).size
75
+ end
76
+
77
+ should 'return records archived after specified date if :after option is passed via rails helper' do
78
+ assert_equal 1, TestModel.archived(:after => 10.seconds.ago).size
79
+ assert_equal 0, TestModel.archived(:after => 10.seconds.since(DateTime.current)).size
80
+ end
81
+
82
+ should 'go through each archived record if block passed' do
83
+ TestModel.archived do |timestamp, attributes|
84
+ assert_equal @record.id.to_s, attributes['id']
85
+ assert_equal @record.firstname, attributes['firstname']
86
+ assert_equal @record.lastname, attributes['lastname']
87
+ assert_equal @record.fullname, attributes['fullname']
88
+
89
+ # we didn't list created_at attribute in cassandra_archive_attributes method, so it should not be archived
90
+ assert_equal nil, attributes['created_at']
91
+ end
92
+ end
93
+ end
94
+
95
+ def find_archived_record(record)
96
+ # find archived record, it returns array with two elements
97
+ # first element is key, second is hash with attributes
98
+ archived_record = record.class.archived.find do |key, value|
99
+ value['id'] == record.id.to_s
100
+ end
101
+
102
+ # return record attributes
103
+ archived_record.last
104
+ end
105
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cassandra_archive
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alexander Litvinovsky
9
+ - Jason Haruska
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-09-06 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ type: :runtime
17
+ name: activerecord
18
+ prerelease: false
19
+ requirement: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ! '>='
22
+ - !ruby/object:Gem::Version
23
+ version: 3.0.0
24
+ none: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 3.0.0
30
+ none: false
31
+ - !ruby/object:Gem::Dependency
32
+ type: :runtime
33
+ name: cassandra
34
+ prerelease: false
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: 0.12.2
40
+ none: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 0.12.2
46
+ none: false
47
+ - !ruby/object:Gem::Dependency
48
+ type: :development
49
+ name: sqlite3
50
+ prerelease: false
51
+ requirement: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ none: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ none: false
63
+ - !ruby/object:Gem::Dependency
64
+ type: :development
65
+ name: shoulda
66
+ prerelease: false
67
+ requirement: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ none: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ none: false
79
+ - !ruby/object:Gem::Dependency
80
+ type: :development
81
+ name: mocha
82
+ prerelease: false
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ none: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ none: false
95
+ description: The library allows to archive destroyed record to cassandra. For that
96
+ moment ActiveRecord is supported.
97
+ email:
98
+ - dev@backupify.com
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - .gitignore
104
+ - Gemfile
105
+ - Gemfile.lock
106
+ - LICENSE.txt
107
+ - README.md
108
+ - Rakefile
109
+ - cassandra_archive.gemspec
110
+ - lib/cassandra_archive.rb
111
+ - lib/cassandra_archive/helper.rb
112
+ - lib/cassandra_archive/version.rb
113
+ - test/schema.rb
114
+ - test/test_helper.rb
115
+ - test/unit/cassandra_archive_test.rb
116
+ homepage: http://github.com/backupify/cassandra_archive
117
+ licenses:
118
+ - MIT
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ! '>='
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ none: false
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ none: false
135
+ requirements: []
136
+ rubyforge_project:
137
+ rubygems_version: 1.8.23
138
+ signing_key:
139
+ specification_version: 3
140
+ summary: Archiving and retrieving records to cassandra
141
+ test_files:
142
+ - test/schema.rb
143
+ - test/test_helper.rb
144
+ - test/unit/cassandra_archive_test.rb
145
+ has_rdoc: