postmodern 0.0.11 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 15bd95a1daf5a24f06fcfa1645094b3bd1a76c0c
4
- data.tar.gz: 60aeb0096180e784f4e13e812075319523609823
3
+ metadata.gz: 1bc358882bdb3ac6f246d3967ac261076b4c334d
4
+ data.tar.gz: 9a3cf7820f70f5271f9ee570be8444bd8305d7e4
5
5
  SHA512:
6
- metadata.gz: fac44addd806dc9688c1cf5a076c2bed5b6ebf6aa44c58d1c78357a9c4046b3735aa13317b0fa890546f6202d054c3a2971bfb7806c09b4cfa5f13a78a16b0f0
7
- data.tar.gz: bc2afb31d22804a418ed917b5bae2d1422cf4740b41ab3cdab0508a4edbaa2a12566afde2c6c445e16fc6f3e5c51504e4d211230baa59e0281c0a6be8adcb836
6
+ metadata.gz: 619b48a8a1b7b6c250455c143dbe2ac9465066269aea935760e51f7ac6ca9393aacb92a4234a39000fa230de7edb6a5261916e4ccb20c50af8b75ae329ba4e6b
7
+ data.tar.gz: 3e98b9eeaaa9e57886b00c653e871d2b9816024a17c4c96089abe22cdc6b582a4c8762b7490a5acff54483674296a0170e17dd3f092dfa4cd5429b2e3ef3924e
data/ATTRIBUTION.md ADDED
@@ -0,0 +1,5 @@
1
+ Attribution
2
+ ===========
3
+
4
+ This gem would not have been possible without the contributions,
5
+ code samples and consultation of PostgreSQL Experts Inc.
data/Gemfile CHANGED
@@ -13,5 +13,6 @@ group :test do
13
13
  gem 'aruba-rspec'
14
14
  gem 'guard-bundler'
15
15
  gem 'guard-rspec'
16
+ gem 'timecop'
16
17
  gem 'terminal-notifier-guard', require: RUBY_PLATFORM.include?('darwin')
17
18
  end
data/README.md CHANGED
@@ -7,6 +7,11 @@ Tools for managing PostgreSQL databases.
7
7
 
8
8
  * WAL archiving and restoration
9
9
 
10
+ ## Dependencies
11
+
12
+ * libpq
13
+ * [pg gem](http://rubygems.org/gems/pg)
14
+
10
15
  ## Installation
11
16
 
12
17
  ```bash
@@ -18,10 +23,62 @@ the system's ruby, however that is installed.
18
23
 
19
24
  ## Usage
20
25
 
26
+ ### Vacuuming and Vacuum Freezing
27
+
28
+ Postmodern's vacuum scripts run table by table, with various constraints
29
+ to limit the overhead of the process.
30
+
31
+ ```
32
+ Usage: postmodern (vacuum|freeze) <options>
33
+ -U, --user USER Defaults to postgres
34
+ -p, --port PORT Defaults to 5432
35
+ -H, --host HOST Defaults to 127.0.0.1
36
+ -W, --password PASS
37
+
38
+ -t, --timeout TIMEOUT Halt after timeout minutes -- default 120
39
+ -d, --database DB Database to vacuum. Required.
40
+
41
+ -B, --tablesize BYTES minimum table size to vacuum -- default 1000000
42
+ -F, --freezeage AGE minimum freeze age -- default 10000000
43
+ -D, --costdelay DELAY vacuum_cost_delay setting in ms -- default 20
44
+ -L, --costlimit LIMIT vacuum_cost_limit setting -- default 2000
45
+
46
+ -h, --help Show this message
47
+ --version Show version
48
+ ```
49
+
50
+ To run a vacuum:
51
+
52
+ ```
53
+ postmodern vacuum -U postgres -p 5432 -d my_database
54
+ ```
55
+
56
+ In order to run vacuum freeze:
57
+
58
+ ```
59
+ postmodern freeze -U postgres -p 5432 -d my_database
60
+ ```
61
+
62
+ These tasks are designed to be run regularly during a window of lower
63
+ database activity. They vacuum or vacuum freeze each table that requires
64
+ it (based on command line options). Before each operation, the scripts check
65
+ to make sure they have not gone longer than `--timeout` seconds.
66
+
21
67
  ### WAL archives
22
68
 
23
69
  The wal archiving scripts packaged in this gem are intended to serve as
24
- wrappers for YOUR archiving mechanism.
70
+ wrappers for YOUR archiving mechanism. Changing the settings for WAL
71
+ archiving in `postgresql.conf` or in `recovery.conf` require full restarts
72
+ of PostgreSQL—using Postmodern, you can configure PostgreSQL once and swap
73
+ in local scripts to do the actual work.
74
+
75
+ ```
76
+ Usage: postmodern (archive|restore) <options>
77
+ -f, --filename FILE File name of xlog
78
+ -p, --path PATH Path of xlog file
79
+ -h, --help Show this message
80
+ --version Show version
81
+ ```
25
82
 
26
83
  In postgresql.conf
27
84
 
@@ -51,6 +108,13 @@ the relevant arguments either as $1, $2 or using the variables listed above.
51
108
  `archive` will attempt to call a `postmodern_archive.local` script.
52
109
  `restore` will attempt to call a `postmodern_restore.local` script.
53
110
 
111
+ see the [examples](https://github.com/wanelo/postmodern/tree/master/examples)
112
+ directory for example local scripts.
113
+
114
+ ## Attribution & Thanks
115
+
116
+ Please see the [attribution](https://github.com/wanelo/postmodern/blob/master/ATTRIBUTION.md)
117
+ file for proper attribution and thanks.
54
118
 
55
119
  ## Contributing
56
120
 
@@ -59,3 +123,13 @@ the relevant arguments either as $1, $2 or using the variables listed above.
59
123
  3. Commit your changes (`git commit -am 'Add some feature'`)
60
124
  4. Push to the branch (`git push origin my-new-feature`)
61
125
  5. Create new Pull Request
126
+
127
+ Contributions will not be accepted without tests. What should be a
128
+ feature and what a unit test is highly open to interpretation, however.
129
+ In some cases, a unit test may be easier and acceptable. In general,
130
+ at least one feature should be written for each new subcommand, even
131
+ if it just runs `--help`.
132
+
133
+ If in doubt, open an issue. If you don't receive a response to an issue
134
+ or a pull request, please mention one of the core committers of this
135
+ gem in a comment to make sure it doesn't get swallowed in an email abyss.
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+
3
+ # Local file for running postgres wal log archiving.
4
+ # Variables are set by postmodern and map to
5
+ # the following wal archiving conventions:
6
+ #
7
+ # WAL_ARCHIVE_PATH = %p
8
+ # WAL_ARCHIVE_FILE = %f
9
+ #
10
+
11
+ WAL_ARCHIVE_PATH=$1
12
+ WAL_ARCHIVE_FILE=$2
13
+
14
+ mkdir -p /var/pgsql/wal_archive && find /var/pgsql/wal_archive/ -mtime +1 \
15
+ | xargs rm -f \
16
+ && test ! -f /var/pgsql/wal_archive/$WAL_ARCHIVE_FILE \
17
+ && cp $WAL_ARCHIVE_PATH /var/pgsql/wal_archive/$WAL_ARCHIVE_FILE
@@ -0,0 +1,14 @@
1
+ #!/bin/bash
2
+
3
+ # Local file for running postgres wal log archiving.
4
+ # Variables are set by postmodern and map to
5
+ # the following wal archiving conventions:
6
+ #
7
+ # WAL_ARCHIVE_PATH = %p
8
+ # WAL_ARCHIVE_FILE = %f
9
+ #
10
+
11
+ WAL_ARCHIVE_PATH=$1
12
+ WAL_ARCHIVE_FILE=$2
13
+
14
+ cp /var/pgsql/wal_archive/$WAL_ARCHIVE_FILE $WAL_ARCHIVE_PATH
data/lib/postmodern.rb CHANGED
@@ -1,4 +1,8 @@
1
1
  require "postmodern/version"
2
+ require 'logger'
2
3
 
3
4
  module Postmodern
5
+ def self.logger
6
+ @logger ||= ::Logger.new(STDOUT)
7
+ end
4
8
  end
@@ -2,18 +2,38 @@ require 'optparse'
2
2
 
3
3
  module Postmodern
4
4
  class Command
5
- def self.required_options
6
- @required_options ||= []
5
+ class << self
6
+ def inherited(subclass)
7
+ subclass.instance_variable_set(:@required_options, @required_options)
8
+ subclass.instance_variable_set(:@default_options, @default_options)
9
+ end
10
+
11
+ def required_options
12
+ @required_options ||= []
13
+ end
14
+
15
+ def required_option(*options)
16
+ required_options.concat(options)
17
+ required_options.uniq!
18
+ end
19
+
20
+ def default_options
21
+ @default_options ||= {}
22
+ end
23
+
24
+ def default_option(key, value)
25
+ default_options[key] = value
26
+ end
7
27
  end
8
28
 
9
29
  def parser
10
- OptionParser.new
30
+ raise "Command needs to define an OptionParser"
11
31
  end
12
32
 
13
33
  attr_reader :options
14
34
 
15
35
  def initialize(args)
16
- @options = {}
36
+ @options = self.class.default_options.dup
17
37
 
18
38
  parse_args(args)
19
39
  validate!
@@ -23,15 +43,23 @@ module Postmodern
23
43
  end
24
44
 
25
45
  def validate!
26
- if (self.class.required_options - self.options.keys).any?
27
- puts parser
28
- exit 1
46
+ if missing_params.any?
47
+ puts "Missing #{missing_params.join(', ')}"
48
+ usage!
29
49
  end
30
50
  end
31
51
 
52
+ def missing_params
53
+ self.class.required_options - self.options.keys
54
+ end
55
+
32
56
  def parse_args(args)
33
57
  parser.parse!(args)
34
- self.options
58
+ end
59
+
60
+ def usage!
61
+ puts parser.to_s
62
+ exit 1
35
63
  end
36
64
  end
37
65
  end
@@ -0,0 +1,30 @@
1
+ require 'pg'
2
+
3
+ module Postmodern
4
+ module DB
5
+ class Adapter
6
+
7
+ attr_reader :config
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ end
12
+
13
+ def pg_adapter
14
+ @pg_adapter ||= PG.connect(db_configuration)
15
+ end
16
+
17
+ def execute(sql)
18
+ pg_adapter.exec(sql)
19
+ end
20
+
21
+ private
22
+
23
+ def db_configuration
24
+ db_configuration = {}.merge(config)
25
+ db_configuration.delete(:password) unless config[:password]
26
+ db_configuration
27
+ end
28
+ end
29
+ end
30
+ end
@@ -2,6 +2,11 @@ require 'postmodern/command'
2
2
 
3
3
  module Postmodern
4
4
  class Dummy < Command
5
+ def run
6
+ puts parser
7
+ exit 1
8
+ end
9
+
5
10
  def parser
6
11
  @parser ||= OptionParser.new do |opts|
7
12
  opts.banner = "Usage: postmodern <command> <options>"
@@ -10,6 +15,7 @@ module Postmodern
10
15
  opts.separator "Available commands:"
11
16
  opts.separator " archive"
12
17
  opts.separator " restore"
18
+ opts.separator " vacuum"
13
19
  opts.separator ""
14
20
  opts.separator "Options:"
15
21
 
@@ -1,4 +1,8 @@
1
- require 'postmodern/wal'
1
+ require 'postmodern'
2
+ require 'postmodern/wal/archive'
3
+ require 'postmodern/wal/restore'
4
+ require 'postmodern/vacuum/vacuum'
5
+ require 'postmodern/vacuum/freeze'
2
6
  require 'postmodern/dummy'
3
7
 
4
8
  module Postmodern
@@ -8,7 +12,9 @@ module Postmodern
8
12
  DEFAULT_COMMAND = Dummy
9
13
  COMMAND_MAP = {
10
14
  'archive' => WAL::Archive,
11
- 'restore' => WAL::Restore
15
+ 'restore' => WAL::Restore,
16
+ 'vacuum' => Vacuum::Vacuum,
17
+ 'freeze' => Vacuum::Freeze
12
18
  }.freeze
13
19
 
14
20
  def self.run(args)
@@ -0,0 +1,33 @@
1
+ require 'postmodern/vacuum/vacuum'
2
+
3
+ module Postmodern
4
+ module Vacuum
5
+ class Freeze < Postmodern::Vacuum::Vacuum
6
+
7
+ def vacuum_statement table_name
8
+ "VACUUM FREEZE ANALYZE %s" % table_name
9
+ end
10
+
11
+ protected
12
+
13
+ def table_sql
14
+ <<-SQL.gsub(/^\s{8}/, '')
15
+ WITH tabfreeze AS (
16
+ SELECT pg_class.oid::regclass AS full_table_name,
17
+ age(relfrozenxid)as freeze_age,
18
+ pg_relation_size(pg_class.oid)
19
+ FROM pg_class JOIN pg_namespace ON relnamespace = pg_namespace.oid
20
+ WHERE nspname not in ('pg_catalog', 'information_schema')
21
+ AND nspname NOT LIKE 'pg_temp%'
22
+ AND relkind = 'r'
23
+ )
24
+ SELECT full_table_name
25
+ FROM tabfreeze
26
+ WHERE freeze_age > #{options[:freezeage]}
27
+ ORDER BY freeze_age DESC
28
+ LIMIT 1000;
29
+ SQL
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,155 @@
1
+ require 'postmodern/command'
2
+ require 'postmodern/db/adapter'
3
+
4
+ module Postmodern
5
+ module Vacuum
6
+ class Vacuum < Postmodern::Command
7
+ default_option :timeout, 120
8
+ default_option :pause, 10
9
+ default_option :tablesize, 1000000
10
+ default_option :freezeage, 10000000
11
+ default_option :costdelay, 20
12
+ default_option :costlimit, 2000
13
+ default_option :user, 'postgres'
14
+ default_option :port, 5432
15
+ default_option :host, '127.0.0.1'
16
+
17
+ required_option :database
18
+
19
+ def parser
20
+ @parser ||= OptionParser.new do |opts|
21
+ opts.banner = "Usage: postmodern (vacuum|freeze) <options>"
22
+
23
+ opts.on('-U', '--user USER', 'Defaults to postgres') do |opt|
24
+ self.options[:user] = opt
25
+ end
26
+
27
+ opts.on('-p', '--port PORT', 'Defaults to 5432') do |opt|
28
+ self.options[:port] = opt
29
+ end
30
+
31
+ opts.on('-H', '--host HOST', 'Defaults to 127.0.0.1') do |opt|
32
+ self.options[:host] = opt
33
+ end
34
+
35
+ opts.on('-W', '--password PASS') do |opt|
36
+ self.options[:password] = opt
37
+ end
38
+
39
+ opts.separator ''
40
+
41
+ opts.on('-t', '--timeout TIMEOUT', 'Halt after timeout minutes -- default 120') do |opt|
42
+ self.options[:timeout] = opt
43
+ end
44
+
45
+ opts.on('-d', '--database DB', 'Database to vacuum. Required.') do |opt|
46
+ self.options[:database] = opt
47
+ end
48
+
49
+ opts.separator ''
50
+
51
+ opts.on('-B', '--tablesize BYTES', 'minimum table size to vacuum -- default 1000000') do |opt|
52
+ self.options[:tablesize] = opt
53
+ end
54
+
55
+ opts.on('-F', '--freezeage AGE', 'minimum freeze age -- default 10000000') do |opt|
56
+ self.options[:freezeage] = opt
57
+ end
58
+
59
+ opts.on('-D', '--costdelay DELAY', 'vacuum_cost_delay setting in ms -- default 20') do |opt|
60
+ self.options[:costdelay] = opt
61
+ end
62
+
63
+ opts.on('-L', '--costlimit LIMIT', 'vacuum_cost_limit setting -- default 2000') do |opt|
64
+ self.options[:costlimit] = opt
65
+ end
66
+
67
+ opts.separator ''
68
+
69
+ opts.on_tail("-h", "--help", "Show this message") do
70
+ puts opts
71
+ exit
72
+ end
73
+
74
+ opts.on_tail("--version", "Show version") do
75
+ require 'postmodern/version'
76
+ puts Postmodern::VERSION
77
+ exit
78
+ end
79
+ end
80
+ end
81
+
82
+ attr_reader :adapter, :start_time
83
+
84
+ def initialize(args)
85
+ @start_time = Time.now
86
+ super(args)
87
+ end
88
+
89
+ def run
90
+ configure_vacuum_cost
91
+ vacuum
92
+ end
93
+
94
+ def configure_vacuum_cost
95
+ adapter.execute("SET vacuum_cost_delay = '%d ms'" % options[:costdelay])
96
+ adapter.execute("SET vacuum_cost_limit = '%d'" % options[:costlimit])
97
+ end
98
+
99
+ def vacuum
100
+ tables_to_vacuum.each do |table|
101
+ Postmodern.logger.info "Vacuuming #{table}"
102
+ adapter.execute(vacuum_statement(table))
103
+ if timedout?
104
+ Postmodern.logger.warn "Vacuuming timed out"
105
+ break
106
+ end
107
+ end
108
+ end
109
+
110
+ def vacuum_statement table_name
111
+ "VACUUM ANALYZE %s" % table_name
112
+ end
113
+
114
+ def timedout?
115
+ Time.now >= start_time + (options[:timeout].to_i * 60)
116
+ end
117
+
118
+ def tables_to_vacuum
119
+ table_res = adapter.execute(table_sql)
120
+ table_res.map{|f| f['full_table_name']}
121
+ end
122
+
123
+ def adapter
124
+ @adapter ||= Postmodern::DB::Adapter.new({
125
+ dbname: options[:database],
126
+ port: options[:port],
127
+ host: options[:host],
128
+ user: options[:user],
129
+ password: options[:password]
130
+ })
131
+ end
132
+
133
+ protected
134
+
135
+ def table_sql
136
+ <<-SQL.gsub(/^\s{8}/, '')
137
+ WITH deadrow_tables AS (
138
+ SELECT relid::regclass as full_table_name,
139
+ ((n_dead_tup::numeric) / ( n_live_tup + 1 )) as dead_pct,
140
+ pg_relation_size(relid) as table_bytes
141
+ FROM pg_stat_user_tables
142
+ WHERE n_dead_tup > 100
143
+ AND ( (now() - last_autovacuum) > INTERVAL '1 hour'
144
+ OR last_autovacuum IS NULL )
145
+ )
146
+ SELECT full_table_name
147
+ FROM deadrow_tables
148
+ WHERE dead_pct > 0.05
149
+ AND table_bytes > #{options[:tablesize]}
150
+ ORDER BY dead_pct DESC, table_bytes DESC;
151
+ SQL
152
+ end
153
+ end
154
+ end
155
+ end
@@ -1,3 +1,3 @@
1
1
  module Postmodern
2
- VERSION = "0.0.11"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -3,15 +3,14 @@ require 'postmodern/command'
3
3
  module Postmodern
4
4
  module WAL
5
5
  class Archive < Postmodern::Command
6
- required_options << :file
7
- required_options << :path
6
+ required_option :filename, :path
8
7
 
9
8
  def parser
10
9
  @parser ||= OptionParser.new do |opts|
11
10
  opts.banner = "Usage: postmodern (archive|restore) <options>"
12
11
 
13
12
  opts.on('-f', '--filename FILE', 'File name of xlog') do |o|
14
- self.options[:file] = o
13
+ self.options[:filename] = o
15
14
  end
16
15
 
17
16
  opts.on('-p', '--path PATH', 'Path of xlog file') do |o|
@@ -49,7 +48,7 @@ module Postmodern
49
48
  end
50
49
 
51
50
  def filename
52
- @options[:file]
51
+ @options[:filename]
53
52
  end
54
53
 
55
54
  def local_script_exists?
data/postmodern.gemspec CHANGED
@@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^spec/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.add_dependency "pg"
22
+
21
23
  spec.add_development_dependency "bundler", "~> 1.5"
22
24
  spec.add_development_dependency "rake"
23
25
  end
@@ -37,7 +37,11 @@ Usage: postmodern (archive|restore) <options>
37
37
  end
38
38
 
39
39
  it 'prints usage' do
40
- expect(command).to eq(usage)
40
+ expect(command).to match(Regexp.escape(usage))
41
+ end
42
+
43
+ it 'includes missing params' do
44
+ expect(command).to match('Missing path')
41
45
  end
42
46
  end
43
47
 
@@ -49,7 +53,11 @@ Usage: postmodern (archive|restore) <options>
49
53
  end
50
54
 
51
55
  it 'prints usage' do
52
- expect(command).to eq(usage)
56
+ expect(command).to match(Regexp.escape(usage))
57
+ end
58
+
59
+ it 'includes missing params' do
60
+ expect(command).to match('Missing filename')
53
61
  end
54
62
  end
55
63
  end
@@ -1,24 +1,43 @@
1
1
  require 'spec_helper'
2
2
  require 'postmodern/version'
3
3
 
4
- describe 'dummy help' do
5
- it 'responds with usage info' do
6
- expect(`bin/postmodern --help`).to eq <<-END
4
+ describe 'dummy' do
5
+ let(:usage) {
6
+ <<-END
7
7
  Usage: postmodern <command> <options>
8
8
 
9
9
  Available commands:
10
10
  archive
11
11
  restore
12
+ vacuum
12
13
 
13
14
  Options:
14
15
  -h, --help Show this message
15
16
  --version Show version
16
17
  END
18
+ }
19
+
20
+ describe 'help' do
21
+ it 'responds with usage info' do
22
+ expect(`bin/postmodern --help`).to eq(usage)
23
+ end
17
24
  end
18
- end
19
25
 
20
- describe 'dummy version' do
21
- it 'responds with the Postmodern version' do
22
- expect(`bin/postmodern --version`).to match(Postmodern::VERSION)
26
+ describe 'version' do
27
+ it 'responds with the Postmodern version' do
28
+ expect(`bin/postmodern --version`).to match(Postmodern::VERSION)
29
+ end
30
+ end
31
+
32
+ describe 'argument catchall' do
33
+ let(:command) { `bin/postmodern dlaskfjdflf` }
34
+
35
+ it 'exits 1' do
36
+ expect { command }.to have_exit_status(1)
37
+ end
38
+
39
+ it 'prints usage' do
40
+ expect(command).to eq(usage)
41
+ end
23
42
  end
24
43
  end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'freeze help' do
4
+ it 'responds' do
5
+ expect(`bin/postmodern freeze --help`).to eq <<-END
6
+ Usage: postmodern (vacuum|freeze) <options>
7
+ -U, --user USER Defaults to postgres
8
+ -p, --port PORT Defaults to 5432
9
+ -H, --host HOST Defaults to 127.0.0.1
10
+ -W, --password PASS
11
+
12
+ -t, --timeout TIMEOUT Halt after timeout minutes -- default 120
13
+ -d, --database DB Database to vacuum. Required.
14
+
15
+ -B, --tablesize BYTES minimum table size to vacuum -- default 1000000
16
+ -F, --freezeage AGE minimum freeze age -- default 10000000
17
+ -D, --costdelay DELAY vacuum_cost_delay setting in ms -- default 20
18
+ -L, --costlimit LIMIT vacuum_cost_limit setting -- default 2000
19
+
20
+ -h, --help Show this message
21
+ --version Show version
22
+ END
23
+ end
24
+ end
@@ -15,10 +15,54 @@ end
15
15
  describe 'restore' do
16
16
  let(:command) { `bin/postmodern restore --path filepath --filename filename` }
17
17
 
18
+ let(:usage) { <<-END
19
+ Usage: postmodern (archive|restore) <options>
20
+ -f, --filename FILE File name of xlog
21
+ -p, --path PATH Path of xlog file
22
+ -h, --help Show this message
23
+ --version Show version
24
+ END
25
+ }
26
+
27
+
18
28
  before do
19
29
  double_cmd('postmodern_restore.local')
20
30
  end
21
31
 
32
+ describe 'validations' do
33
+ context 'with no --path' do
34
+ let(:command) { `bin/postmodern restore --filename filename` }
35
+
36
+ it 'fails' do
37
+ expect { command }.to have_exit_status(1)
38
+ end
39
+
40
+ it 'prints usage' do
41
+ expect(command).to match(Regexp.escape(usage))
42
+ end
43
+
44
+ it 'includes missing params' do
45
+ expect(command).to match('Missing path')
46
+ end
47
+ end
48
+
49
+ context 'with no --filename' do
50
+ let(:command) { `bin/postmodern restore --path path` }
51
+
52
+ it 'fails' do
53
+ expect { command }.to have_exit_status(1)
54
+ end
55
+
56
+ it 'prints usage' do
57
+ expect(command).to match(Regexp.escape(usage))
58
+ end
59
+
60
+ it 'includes missing params' do
61
+ expect(command).to match('Missing filename')
62
+ end
63
+ end
64
+ end
65
+
22
66
  context 'when local script is present' do
23
67
  before { double_cmd('which', exit: 0) }
24
68
 
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'vacuum help' do
4
+ it 'responds' do
5
+ expect(`bin/postmodern vacuum --help`).to eq <<-END
6
+ Usage: postmodern (vacuum|freeze) <options>
7
+ -U, --user USER Defaults to postgres
8
+ -p, --port PORT Defaults to 5432
9
+ -H, --host HOST Defaults to 127.0.0.1
10
+ -W, --password PASS
11
+
12
+ -t, --timeout TIMEOUT Halt after timeout minutes -- default 120
13
+ -d, --database DB Database to vacuum. Required.
14
+
15
+ -B, --tablesize BYTES minimum table size to vacuum -- default 1000000
16
+ -F, --freezeage AGE minimum freeze age -- default 10000000
17
+ -D, --costdelay DELAY vacuum_cost_delay setting in ms -- default 20
18
+ -L, --costlimit LIMIT vacuum_cost_limit setting -- default 2000
19
+
20
+ -h, --help Show this message
21
+ --version Show version
22
+ END
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+ require 'postmodern/command'
3
+
4
+ describe Postmodern::Command do
5
+ let(:option_parser) { double(parse!: true, to_s: nil) }
6
+ subject(:command_class) { Class.new(Postmodern::Command) }
7
+
8
+ before do
9
+ local_scope_option_parser = option_parser
10
+ command_class.send(:define_method, :parser) { local_scope_option_parser }
11
+ end
12
+
13
+
14
+ describe '#usage!' do
15
+ it 'exits 1' do
16
+ expect { command_class.new([]).usage! }.to raise_error(SystemExit)
17
+ end
18
+
19
+ it 'prints parser' do
20
+ expect { command_class.new([]).usage! }.to raise_error(SystemExit)
21
+ expect(option_parser).to have_received(:to_s)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+ require 'postmodern/db/adapter'
3
+
4
+ describe Postmodern::DB::Adapter do
5
+ subject(:adapter) { Postmodern::DB::Adapter.new(configuration) }
6
+
7
+ let(:password) { 'secure...' }
8
+ let(:configuration) do
9
+ {
10
+ stuff: 'things'
11
+ }
12
+ end
13
+
14
+ describe '#adapter' do
15
+ context 'with password' do
16
+ let(:configuration) { {stuff: 'things', password: 'password'} }
17
+
18
+ it 'initializes a PG adapter' do
19
+ expect(PG).to receive(:connect).with({stuff: 'things', password: 'password'})
20
+ adapter.pg_adapter
21
+ end
22
+ end
23
+
24
+ context 'without password' do
25
+ let(:configuration) { {stuff: 'things', password: nil} }
26
+
27
+ it 'initializes a PG adapter without password params' do
28
+ expect(PG).to receive(:connect).with({stuff: 'things'})
29
+ adapter.pg_adapter
30
+ end
31
+ end
32
+ end
33
+
34
+ describe '#execute' do
35
+ let(:pg_adapter) { double }
36
+
37
+ before do
38
+ allow(adapter).to receive(:pg_adapter).and_return(pg_adapter)
39
+ end
40
+
41
+ it 'delegates to PG adapter' do
42
+ expect(pg_adapter).to receive(:exec).with('blah;')
43
+ adapter.execute('blah;')
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+ require 'postmodern'
3
+
4
+ describe Postmodern do
5
+ describe '.logger' do
6
+ it 'is a Logger' do
7
+ expect(Postmodern.logger).to be_a Logger
8
+ end
9
+ end
10
+ end
@@ -12,6 +12,11 @@ describe Postmodern::Runner do
12
12
  expect(Postmodern::Runner.command_for('restore')).to be Postmodern::WAL::Restore
13
13
  end
14
14
 
15
+
16
+ it 'chooses vacuumer' do
17
+ expect(Postmodern::Runner.command_for('vacuum')).to be Postmodern::Vacuum::Vacuum
18
+ end
19
+
15
20
  it 'defaults to dummy' do
16
21
  expect(Postmodern::Runner.command_for('ljfaldf')).to be Postmodern::Dummy
17
22
  end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+ require 'postmodern/vacuum/freeze'
3
+
4
+ describe Postmodern::Vacuum::Freeze do
5
+ let(:adapter) { double }
6
+ let(:args) { %w(-d db) }
7
+ subject(:command) { Postmodern::Vacuum::Freeze.new(args) }
8
+
9
+ before do
10
+ allow(Postmodern).to receive(:logger).and_return(FakeLogger.new)
11
+ allow(adapter).to receive(:execute)
12
+ allow(command).to receive(:adapter).and_return(adapter)
13
+ end
14
+
15
+ describe '#tables_to_vacuum' do
16
+ let(:args) { %w(-d mydb -F 12345) }
17
+
18
+ it 'finds list of tables to vacuum' do
19
+ result = [
20
+ {'full_table_name' => 'table1'},
21
+ {'full_table_name' => 'table2'},
22
+ ]
23
+ allow(command).to receive(:adapter).and_return(adapter)
24
+ expect(adapter).to receive(:execute).with(%Q{WITH tabfreeze AS (
25
+ SELECT pg_class.oid::regclass AS full_table_name,
26
+ age(relfrozenxid)as freeze_age,
27
+ pg_relation_size(pg_class.oid)
28
+ FROM pg_class JOIN pg_namespace ON relnamespace = pg_namespace.oid
29
+ WHERE nspname not in ('pg_catalog', 'information_schema')
30
+ AND nspname NOT LIKE 'pg_temp%'
31
+ AND relkind = 'r'
32
+ )
33
+ SELECT full_table_name
34
+ FROM tabfreeze
35
+ WHERE freeze_age > 12345
36
+ ORDER BY freeze_age DESC
37
+ LIMIT 1000;
38
+ }).and_return(result)
39
+
40
+ expect(command.tables_to_vacuum).to eq(%w(table1 table2))
41
+ end
42
+ end
43
+
44
+ describe '#vacuum' do
45
+ let(:tables_to_vacuum) { %w(table1 table2 table3) }
46
+
47
+ before do
48
+ allow(Postmodern::DB::Adapter).to receive(:new).and_return(adapter)
49
+ allow(command).to receive(:tables_to_vacuum).and_return(tables_to_vacuum)
50
+ end
51
+
52
+ it "vacuums each table" do
53
+ command.vacuum
54
+ tables_to_vacuum.each do |table|
55
+ expect(adapter).to have_received(:execute).with("VACUUM FREEZE ANALYZE %s" % table)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,186 @@
1
+ require 'spec_helper'
2
+ require 'postmodern/vacuum/vacuum'
3
+ require 'timecop'
4
+
5
+ describe Postmodern::Vacuum::Vacuum do
6
+ let(:args) { %w(-d mydb) }
7
+ let(:adapter) { double }
8
+ subject(:command) { Postmodern::Vacuum::Vacuum.new(args) }
9
+
10
+ before do
11
+ allow(Postmodern).to receive(:logger).and_return(FakeLogger.new)
12
+ allow(adapter).to receive(:execute)
13
+ end
14
+
15
+ describe "#options" do
16
+ {
17
+ user: 'postgres',
18
+ port: 5432,
19
+ host: '127.0.0.1',
20
+ timeout: 120,
21
+ pause: 10,
22
+ tablesize: 1000000,
23
+ freezeage: 10000000,
24
+ costdelay: 20,
25
+ costlimit: 2000
26
+ }.each do |option, default|
27
+ it "defaults on #{option}" do
28
+ expect(command.options[option]).to eq(default)
29
+ end
30
+ end
31
+ end
32
+
33
+ describe 'validations' do
34
+ let(:usage) { double(usage!: '') }
35
+
36
+ before do
37
+ allow_any_instance_of(Postmodern::Vacuum::Vacuum).
38
+ to receive(:usage!) { usage.usage! }
39
+ end
40
+
41
+ describe 'database' do
42
+ it 'requires database' do
43
+ Postmodern::Vacuum::Vacuum.new([])
44
+ expect(usage).to have_received(:usage!)
45
+ end
46
+ end
47
+ end
48
+
49
+ describe "#run" do
50
+ let(:args) { %w(--database mydb) }
51
+
52
+ before do
53
+ allow(command).to receive(:adapter).and_return(adapter)
54
+ end
55
+
56
+ it 'executes vacuum operations in order' do
57
+ expect(command).to receive(:configure_vacuum_cost).once
58
+ expect(command).to receive(:vacuum).once
59
+ command.run
60
+ end
61
+ end
62
+
63
+ describe '#adapter' do
64
+ before { allow(adapter).to receive(:execute) }
65
+
66
+ it 'instantiates a DB::Adapter' do
67
+ expect(Postmodern::DB::Adapter).to receive(:new).once.with({
68
+ dbname: 'mydb',
69
+ port: 5432,
70
+ host: '127.0.0.1',
71
+ user: 'postgres',
72
+ password: nil
73
+ }).and_return(adapter)
74
+ command.adapter
75
+ end
76
+
77
+ context 'when password is present' do
78
+ let(:args) { %w(--password mypass --database mydb) }
79
+ it 'passes through to adapter' do
80
+ expect(Postmodern::DB::Adapter).to receive(:new).once.with({
81
+ dbname: 'mydb',
82
+ port: 5432,
83
+ host: '127.0.0.1',
84
+ user: 'postgres',
85
+ password: 'mypass'
86
+ }).and_return(adapter)
87
+ command.adapter
88
+ end
89
+ end
90
+ end
91
+
92
+ describe '#configure_vacuum_cost' do
93
+ before do
94
+ allow(command).to receive(:adapter).and_return(adapter)
95
+ command.configure_vacuum_cost
96
+ end
97
+
98
+ it 'sets vacuum cost delay' do
99
+ expect(adapter).to have_received(:execute).with("SET vacuum_cost_delay = '#{command.options[:costdelay]} ms'")
100
+ end
101
+
102
+ it 'sets vacuum cost limit' do
103
+ expect(adapter).to have_received(:execute).with("SET vacuum_cost_limit = '#{command.options[:costlimit]}'")
104
+ end
105
+ end
106
+
107
+ describe '#tables_to_vacuum' do
108
+ let(:args) { %w(-d mydb -B 12345) }
109
+
110
+ it 'finds list of tables to vacuum' do
111
+ result = [
112
+ { 'full_table_name' => 'table1'},
113
+ { 'full_table_name' => 'table2'},
114
+ ]
115
+ allow(command).to receive(:adapter).and_return(adapter)
116
+ expect(adapter).to receive(:execute).with(%Q{WITH deadrow_tables AS (
117
+ SELECT relid::regclass as full_table_name,
118
+ ((n_dead_tup::numeric) / ( n_live_tup + 1 )) as dead_pct,
119
+ pg_relation_size(relid) as table_bytes
120
+ FROM pg_stat_user_tables
121
+ WHERE n_dead_tup > 100
122
+ AND ( (now() - last_autovacuum) > INTERVAL '1 hour'
123
+ OR last_autovacuum IS NULL )
124
+ )
125
+ SELECT full_table_name
126
+ FROM deadrow_tables
127
+ WHERE dead_pct > 0.05
128
+ AND table_bytes > 12345
129
+ ORDER BY dead_pct DESC, table_bytes DESC;
130
+ }).and_return(result)
131
+
132
+ expect(command.tables_to_vacuum).to eq(%w(table1 table2))
133
+ end
134
+ end
135
+
136
+ describe "#vacuum" do
137
+ before do
138
+ allow(Postmodern::DB::Adapter).to receive(:new).and_return(adapter)
139
+ allow(command).to receive(:tables_to_vacuum).and_return(tables_to_vacuum)
140
+ end
141
+
142
+ let(:tables_to_vacuum) { %w(table1 table2 table3) }
143
+
144
+ it "vacuums each table" do
145
+ command.vacuum
146
+ tables_to_vacuum.each do |table|
147
+ expect(adapter).to have_received(:execute).with("VACUUM ANALYZE %s" % table)
148
+ end
149
+ end
150
+
151
+ it "exits prematurely with a timeout and analyzes first table" do
152
+ allow(command).to receive(:timedout?).and_return(true)
153
+ command.vacuum
154
+ expect(adapter).to have_received(:execute).with("VACUUM ANALYZE %s" % tables_to_vacuum[0])
155
+ expect(adapter).not_to have_received(:execute).with("VACUUM ANALYZE %s" % tables_to_vacuum[1])
156
+ expect(adapter).not_to have_received(:execute).with("VACUUM ANALYZE %s" % tables_to_vacuum[2])
157
+ end
158
+ end
159
+
160
+ describe '#timedout?' do
161
+ let(:time) { Time.now }
162
+ let(:args) { %w(--d db -t 15) }
163
+
164
+ before do
165
+ Timecop.freeze time do
166
+ command # ensure command is initialized Now
167
+ end
168
+ end
169
+
170
+ context 'current time is greater than timeout threshold since initialization' do
171
+ it 'is true' do
172
+ Timecop.freeze time + (15 * 60) do
173
+ expect(command.timedout?).to be true
174
+ end
175
+ end
176
+ end
177
+
178
+ context 'current time is less than timeout threshold since initialization' do
179
+ it 'is false' do
180
+ Timecop.freeze time + (15 * 60 - 1) do
181
+ expect(command.timedout?).to be false
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  require 'aruba/rspec'
2
2
  require 'pry'
3
+ require 'timecop'
4
+
5
+ require 'support/logger'
3
6
 
4
7
  RSpec.configure do |config|
5
8
  config.treat_symbols_as_metadata_keys_with_true_values = true
@@ -16,5 +19,6 @@ RSpec.configure do |config|
16
19
 
17
20
  config.after :each do
18
21
  Aruba::RSpec.teardown
22
+ Timecop.return
19
23
  end
20
24
  end
@@ -0,0 +1,9 @@
1
+ class FakeLogger
2
+ def info(*args)
3
+
4
+ end
5
+
6
+ def warn(*args)
7
+
8
+ end
9
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: postmodern
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.11
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Saxby
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-05-29 00:00:00.000000000 Z
11
+ date: 2014-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pg
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -49,28 +63,41 @@ files:
49
63
  - ".gitignore"
50
64
  - ".rspec"
51
65
  - ".travis.yml"
66
+ - ATTRIBUTION.md
52
67
  - Gemfile
53
68
  - Guardfile
54
69
  - LICENSE.txt
55
70
  - README.md
56
71
  - Rakefile
57
72
  - bin/postmodern
73
+ - examples/postmodern_archive.local
74
+ - examples/postmodern_restore.local
58
75
  - lib/postmodern.rb
59
76
  - lib/postmodern/command.rb
77
+ - lib/postmodern/db/adapter.rb
60
78
  - lib/postmodern/dummy.rb
61
79
  - lib/postmodern/runner.rb
80
+ - lib/postmodern/vacuum/freeze.rb
81
+ - lib/postmodern/vacuum/vacuum.rb
62
82
  - lib/postmodern/version.rb
63
- - lib/postmodern/wal.rb
64
83
  - lib/postmodern/wal/archive.rb
65
84
  - lib/postmodern/wal/restore.rb
66
85
  - postmodern.gemspec
67
86
  - spec/features/archive_spec.rb
68
87
  - spec/features/dummy_spec.rb
88
+ - spec/features/freeze_spec.rb
69
89
  - spec/features/restore_spec.rb
90
+ - spec/features/vacuum_spec.rb
91
+ - spec/postmodern/command_spec.rb
92
+ - spec/postmodern/db/adapter_spec.rb
93
+ - spec/postmodern/postmodern_spec.rb
70
94
  - spec/postmodern/runner_spec.rb
95
+ - spec/postmodern/vacuum/freeze_spec.rb
96
+ - spec/postmodern/vacuum/vacuum_spec.rb
71
97
  - spec/postmodern/wal/archive_spec.rb
72
98
  - spec/postmodern/wal/restore_spec.rb
73
99
  - spec/spec_helper.rb
100
+ - spec/support/logger.rb
74
101
  homepage: https://github.com/wanelo/postmodern
75
102
  licenses:
76
103
  - MIT
@@ -98,9 +125,17 @@ summary: Tools for managing PostgreSQL
98
125
  test_files:
99
126
  - spec/features/archive_spec.rb
100
127
  - spec/features/dummy_spec.rb
128
+ - spec/features/freeze_spec.rb
101
129
  - spec/features/restore_spec.rb
130
+ - spec/features/vacuum_spec.rb
131
+ - spec/postmodern/command_spec.rb
132
+ - spec/postmodern/db/adapter_spec.rb
133
+ - spec/postmodern/postmodern_spec.rb
102
134
  - spec/postmodern/runner_spec.rb
135
+ - spec/postmodern/vacuum/freeze_spec.rb
136
+ - spec/postmodern/vacuum/vacuum_spec.rb
103
137
  - spec/postmodern/wal/archive_spec.rb
104
138
  - spec/postmodern/wal/restore_spec.rb
105
139
  - spec/spec_helper.rb
140
+ - spec/support/logger.rb
106
141
  has_rdoc:
@@ -1,3 +0,0 @@
1
- require 'postmodern/wal/archive'
2
- require 'postmodern/wal/restore'
3
-