postmodern 0.0.11 → 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.
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
-