buildcache 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 30a10ca2c786761a31c76f72b11a13d1f5b0cbdd
4
+ data.tar.gz: 1088fe6a3088038d904d1dff0d9e403486d7479b
5
+ SHA512:
6
+ metadata.gz: f013dd48f41f66bac7ecdc0f6d9cbcc58df8205766791f5fd86b2b0f58d0ea6cbb702948c1e89384ef2927a1b037e0e4f77f85f51c40746805473512fdfee15a
7
+ data.tar.gz: a92855f76154bbd1ff65b39c25d474b9cb3811bc2979e835a6e79e67dee6a93460994b8d52c699c69241401d1fb164d51aab13ffc4ffd3c46a471f02cc5c4aef
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.1.1
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in buildcache.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Victor Lyuboslavsky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,78 @@
1
+ [![Build Status](https://travis-ci.org/getvictor/buildcache.svg?branch=master)](https://travis-ci.org/getvictor/buildcache)
2
+
3
+ # BuildCache
4
+
5
+ A simple cache for files. It is currently used in production to cache the results of frequently run built steps (using a custom build flow).
6
+
7
+ The cache receives the input files and some metadata. It generates a hash key and checks the cache. If the cache is hit, the files are copied from the cache to the provided location. If we have a cache miss, the provided block is run, and the resulting files are copied to the cache.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'buildcache'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install buildcache
24
+
25
+ ## Usage
26
+
27
+ ### Block Style
28
+
29
+ :::ruby
30
+ require 'buildcache'
31
+ @my_cache = BuildCache::DiskCache.new('/tmp/cache')
32
+ @input_files = ['./input.txt']
33
+ @metadata = {:cmd => 'parse input'}
34
+ @dest_dir = './build'
35
+ @my_cache.cache(@input_files, @metadata, @dest_dir) do
36
+ # This is time consuming code where you generate result files.
37
+ # The block is only run on a cache miss
38
+ ...
39
+ # You must return resulting files so they can be cached
40
+ next files
41
+ end
42
+
43
+ ### Use API
44
+
45
+ The cache uses 2 keys. The `first_key` corresponds to directory name in the cache. `second_key` (optional) is held in a file and is used to resolve hash conflicts on first_key.
46
+
47
+ The following methods are available:
48
+
49
+ :::ruby
50
+ # Generate first_key
51
+ first_key = BuildCache.key_gen(input_files_array, metadata)
52
+
53
+ :::ruby
54
+ # Check for a cache hit
55
+ @my_cache.hit?(first_key, second_key)
56
+
57
+ :::ruby
58
+ # Set the cache
59
+ @my_cache.set(first_key, second_key, files_array)
60
+
61
+ :::ruby
62
+ # Retrieve from cache. The result is a cache directory where the files are stored
63
+ contents_dir = @my_cache.get(first_key, second_key)
64
+
65
+ ## Development
66
+
67
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
68
+
69
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
70
+
71
+ ## Contributing
72
+
73
+ Bug reports and pull requests are welcome on GitHub at https://github.com/getvictor/buildcache.
74
+
75
+ ## License
76
+
77
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
78
+
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ Dir.glob('tasks/**/*.rake').each(&method(:import))
4
+
5
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "buildcache"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'buildcache/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "buildcache"
8
+ spec.version = BuildCache::VERSION
9
+ spec.authors = ["Victor Lyuboslavsky"]
10
+
11
+ spec.summary = %q{Build cache for any build system.}
12
+ spec.homepage = "https://github.com/getvictor/buildcache"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
+ spec.bindir = "exe"
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.12"
21
+ spec.add_development_dependency "simplecov", "~> 0.11.2"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", "~> 3.0"
24
+ end
@@ -0,0 +1,233 @@
1
+ require 'buildcache/version'
2
+ require 'digest'
3
+
4
+ module BuildCache
5
+
6
+ def self.key_gen files=[], metadata={}
7
+ # TODO: Validate inputs.
8
+ flat_sig = ''
9
+ unless files.empty?
10
+ files.each { |file|
11
+ flat_sig << Digest::MD5.file(file).hexdigest
12
+ }
13
+ end
14
+ unless metadata.empty?
15
+ flat_sig << Digest::MD5.hexdigest(metadata.to_s)
16
+ end
17
+ return Digest::MD5.hexdigest(flat_sig)
18
+ end
19
+
20
+ class DiskCache
21
+
22
+ # The directory containing cached files
23
+ attr_reader :dir
24
+
25
+ # The Linux permissions that cached files should have
26
+ attr_reader :permissions
27
+
28
+ # logger to use, if logger is not set, then messages will not be logged
29
+ attr_accessor :logger
30
+
31
+ # Percent of time to check the cache size
32
+ attr_accessor :check_size_percent
33
+
34
+ # The maximum number of entries in the cache.
35
+ # Note that since we don't check the cache size every time, the actual size might exceed this number
36
+ attr_accessor :max_cache_size
37
+
38
+ # The percent of entries to evict (delete) if size is exceeded
39
+ attr_accessor :evict_percent
40
+
41
+ def initialize dir='/tmp/cache', permissions=0666
42
+ # Make sure 'dir' is not a file
43
+ if (File.exist?(dir) && !File.directory?(dir))
44
+ raise "DiskCache dir #{dir} should be a directory."
45
+ end
46
+ @dir = dir
47
+ @permissions = permissions
48
+ @enable_logging = false
49
+ @check_size_percent = 20
50
+ @max_cache_size = 5000
51
+ @evict_percent = 20.0
52
+ mkdir
53
+ end
54
+
55
+ def set first_key, second_key='', files=[]
56
+ # TODO: validate inputs
57
+
58
+ # If cache exists already, overwrite it.
59
+ content_dir = get first_key, second_key
60
+ second_key_file = nil
61
+
62
+ begin
63
+ if (content_dir.nil?)
64
+
65
+ # Check the size of cache, and evict entries if too large
66
+ check_cache_size if (rand(100) < check_size_percent)
67
+
68
+ # Make sure cache dir doesn't exist already
69
+ first_cache_dir = File.join(dir, first_key)
70
+ if (File.exist?first_cache_dir)
71
+ raise "BuildCache directory #{first_cache_dir} should be a directory" unless File.directory?(first_cache_dir)
72
+ else
73
+ FileUtils.mkpath(first_cache_dir)
74
+ end
75
+ num_second_dirs = Dir[first_cache_dir + '/*'].length
76
+ cache_dir = File.join(first_cache_dir, num_second_dirs.to_s)
77
+ # If cache directory already exists, then a directory must have been evicted here, so we pick another name
78
+ while File.directory?cache_dir
79
+ cache_dir = File.join(first_cache_dir, rand(num_second_dirs).to_s)
80
+ end
81
+ content_dir = File.join(cache_dir, '/content')
82
+ FileUtils.mkpath(content_dir)
83
+
84
+ # Create 'last_used' file
85
+ last_used_filename = File.join(cache_dir, 'last_used')
86
+ FileUtils.touch last_used_filename
87
+ FileUtils.chmod(permissions, last_used_filename)
88
+
89
+ # Copy second key
90
+ second_key_file = File.open(cache_dir + '/second_key', 'w+')
91
+ second_key_file.flock(File::LOCK_EX)
92
+ second_key_file.write(second_key)
93
+
94
+ else
95
+ log "overwriting cache #{content_dir}"
96
+
97
+ FileUtils.touch content_dir + '/../last_used'
98
+ second_key_file = File.open(content_dir + '/../second_key', 'r')
99
+ second_key_file.flock(File::LOCK_EX)
100
+ # Clear any existing files out of cache directory
101
+ FileUtils.rm_rf(content_dir + '/.')
102
+ end
103
+
104
+ # Copy files into content_dir
105
+ files.each do |filename|
106
+ FileUtils.cp(filename, content_dir)
107
+ end
108
+ FileUtils.chmod(permissions, Dir[content_dir + '/*'])
109
+
110
+ # Release the lock
111
+ second_key_file.close
112
+ return content_dir
113
+ rescue => e
114
+ # Something went wrong, like a full disk or some other error.
115
+ # Delete any work so we don't leave cache in corrupted state
116
+ unless content_dir.nil?
117
+ # Delete parent of content directory
118
+ FileUtils.rm_rf(File.expand_path('..', content_dir))
119
+ end
120
+ log "ERROR: Could not set cache entry. #{e.to_s}"
121
+ return 'ERROR: !NOT CACHED!'
122
+ end
123
+
124
+ end
125
+
126
+ # Get the cache directory containing the contents corresponding to the keys
127
+ def get first_key, second_key=''
128
+ # TODO: validate inputs
129
+
130
+ begin
131
+ cache_dirs = Dir[File.join(@dir, first_key + '/*')]
132
+ cache_dirs.each do |cache_dir|
133
+ second_key_filename = cache_dir + '/second_key'
134
+ # If second key filename is bad, we skip this directory
135
+ if (!File.exist?(second_key_filename) || File.directory?(second_key_filename))
136
+ next
137
+ end
138
+ second_key_file = File.open(second_key_filename, "r" )
139
+ second_key_file.flock(File::LOCK_SH)
140
+ out = second_key_file.read
141
+ if (second_key.to_s == out)
142
+ FileUtils.touch cache_dir + '/last_used'
143
+ cache_dir = File.join(cache_dir, 'content')
144
+ second_key_file.close
145
+ return cache_dir if File.directory?(cache_dir)
146
+ end
147
+ second_key_file.close
148
+ end
149
+ rescue => e
150
+ log "ERROR: Could not get cache entry. #{e.to_s}"
151
+ end
152
+ return nil
153
+ end
154
+
155
+ def hit? first_key, second_key=''
156
+ return get(first_key, second_key) != nil
157
+ end
158
+
159
+ def cache input_files, metadata, dest_dir
160
+ # Create the cache keys
161
+ first_key = BuildCache.key_gen input_files, metadata
162
+ second_key = metadata.to_s
163
+
164
+ # If cache hit, copy the files to the dest_dir
165
+ if (hit?first_key, second_key)
166
+ begin
167
+ cache_dir = get first_key, second_key
168
+ log "cache hit #{cache_dir}"
169
+ mkdir dest_dir
170
+ FileUtils.cp_r(cache_dir + '/.', dest_dir)
171
+ return Dir[cache_dir + '/*'].map { |pathname| File.basename pathname }
172
+ rescue => e
173
+ # Since we don't return, error counts as a cache miss
174
+ log "ERROR: Could not retrieve cache entry contents. #{e.to_s}"
175
+ end
176
+ end
177
+
178
+ # If cache miss, run the block and put the results in the cache
179
+ files = yield
180
+ output_files = files.map { |filename| File.join(dest_dir, filename) }
181
+ # Check the cache again in case someone else populated it already
182
+ unless (hit?first_key, second_key)
183
+ cache_dir = set(first_key, second_key, output_files)
184
+ log "cache miss, caching results to #{cache_dir}"
185
+ end
186
+ return files
187
+ end
188
+
189
+ def check_cache_size
190
+ log "checking cache size"
191
+ entries = Dir[@dir + '/*/*']
192
+ if entries.length > max_cache_size
193
+ # If cache is locked for maintainance (lock exists and less than 8 hours old), then we skip the check
194
+ lock_filename = File.join(@dir, 'cache_maintenance')
195
+ return if (File.exist?(lock_filename) && (File.mtime(lock_filename) > Time.now - (8 * 60 * 60)))
196
+ FileUtils.touch(lock_filename)
197
+
198
+ log "evicting old cache entries"
199
+ # evict some entries
200
+ entries = entries.sort do |a,b|
201
+ # evict entries that don't have a last_used file
202
+ a_file = File.join(a, 'last_used')
203
+ next -1 if !File.exist?a_file
204
+ b_file = File.join(b, 'last_used')
205
+ next 1 if !File.exist?b_file
206
+ next File.mtime(a_file) <=> File.mtime(b_file)
207
+ end
208
+
209
+ entries_to_delete = (entries.length * evict_percent / 100).ceil
210
+ entries[0..(entries_to_delete-1)].each { |entry| FileUtils.rm_rf(entry) }
211
+ # Delete empty directories
212
+ Dir[@dir + '/*'].each { |d| Dir.rmdir d if (File.directory?(d) && (Dir.entries(d) - %w[ . .. ]).empty?) }
213
+
214
+ # Delete lock file
215
+ FileUtils.rm lock_filename, :force => true
216
+ end
217
+ end
218
+
219
+ private
220
+
221
+ def mkdir dir=@dir
222
+ FileUtils.mkpath(dir) unless File.directory?(dir)
223
+ end
224
+
225
+ def log message
226
+ unless (@logger.nil?)
227
+ @logger.info { "[BuildCache::DiskCache] #{message.to_s}" }
228
+ end
229
+ end
230
+
231
+ end
232
+
233
+ end
@@ -0,0 +1,3 @@
1
+ module BuildCache
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,3 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: buildcache
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Victor Lyuboslavsky
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-06-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: simplecov
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.11.2
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.11.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description:
70
+ email:
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - ".gitignore"
76
+ - ".rspec"
77
+ - ".travis.yml"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - bin/console
83
+ - bin/setup
84
+ - buildcache.gemspec
85
+ - lib/buildcache.rb
86
+ - lib/buildcache/version.rb
87
+ - tasks/rspec.rake
88
+ homepage: https://github.com/getvictor/buildcache
89
+ licenses:
90
+ - MIT
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.2.2
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Build cache for any build system.
112
+ test_files: []
113
+ has_rdoc: