# frozen_string_literal: true
#
# Cookbook:: postgresql
# Recipe:: config_pgtune
# Author:: David Crane (<davidc@donorschoose.org>)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

#######
# Load the binaryround(value) method from libraries/default.rb
::Chef::Recipe.send(:include, Opscode::PostgresqlHelpers)

#######
# This recipe is based on Greg Smith's pgtune script (the Feb 1, 2012
# version at https://github.com/gregs1104/pgtune). Introduction: pgtune
# takes the wimpy default postgresql.conf and expands the database
# server to be as powerful as the hardware it's being deployed on.
#
# The default postgresql.conf aims at a system with approximately 128MB
# of RAM. This recipe recommends a baseline configuration in the right
# general range for a dedicated Postgresql system.
#
# This recipe takes three optional parameters that may be passed in as
# node['postgresql']['config_pgtune'] attributes:
#   * db_type -- Specifies database type as one of: dw, oltp,
#     web, mixed, desktop. If not specified, the default is mixed.
#   * max_connections -- Specifies number of maximum connections
#     expected. If not specified, it depends on database type.
#   * total_memory -- Specifies total system memory. If not specified,
#     it will be detected from the Ohai automatic attributes.
#
# Using those inputs, this recipe will compute and set the following
# node.default['postgresql']['config'] attributes:
#   * max_connections
#   * shared_buffers
#   * effective_cache_size
#   * work_mem
#   * maintenance_work_mem
#   * checkpoint_segments
#   * checkpoint_completion_target
#   * wal_buffers
#   * default_statistics_target
#
# This recipe deviates from the original pgtune script for 2 settings:
# shared_buffers is capped for large memory systems (which Greg
# mentioned in a TODO.rst) and wal_buffers will auto-tune starting with
# 9.1 (which is a feature that Greg built into Postgresql).

#######
# These are the workload characteristics of the five database types
# that can be specified as node['postgresql']['config_pgtune']['db_type']:
#
# dw -- Data Warehouse
#   * Typically I/O- or RAM-bound
#   * Large bulk loads of data
#   * Large complex reporting queries
#   * Also called "Decision Support" or "Business Intelligence"
#
# oltp -- Online Transaction Processing
#   * Typically CPU- or I/O-bound
#   * DB slightly larger than RAM to 1TB
#   * 20-40% small data write queries
#   * Some long transactions and complex read queries
#
# web -- Web Application
#   * Typically CPU-bound
#   * DB much smaller than RAM
#   * 90% or more simple queries
#
# mixed -- Mixed DW and OLTP characteristics
#   * A wide mixture of queries
#
# desktop -- Not a dedicated database
#   * A general workstation, perhaps for a developer

# Parse out db_type option, or use default.
db_type = 'mixed'

if node['postgresql'].attribute?('config_pgtune') && node['postgresql']['config_pgtune'].attribute?('db_type')
  db_type = node['postgresql']['config_pgtune']['db_type']
  unless %w(dw oltp web mixed desktop).include?(db_type)
    Chef::Log.fatal([
      "Bad value (#{db_type})",
      "for node['postgresql']['config_pgtune']['db_type'] attribute.",
      'Valid values are one of dw, oltp, web, mixed, desktop.',
    ].join(' '))
    raise
  end
end

# Parse out max_connections option, or use a value based on db_type.
con =
  { 'web' => 200,
    'oltp' => 300,
    'dw' => 20,
    'mixed' => 80,
    'desktop' => 5,
  }.fetch(db_type)

if node['postgresql'].attribute?('config_pgtune') && node['postgresql']['config_pgtune'].attribute?('max_connections')
  max_connections = node['postgresql']['config_pgtune']['max_connections'].to_i
  if max_connections <= 0
    Chef::Log.fatal([
      "Bad value (#{max_connections})",
      "for node['postgresql']['config_pgtune']['max_connections'] attribute.",
      'Valid values are non-zero integers only.',
    ].join(' '))
    raise
  end
  con = max_connections
end

# Parse out total_memory option, or use value detected by Ohai.
total_memory = node['memory']['total']

# Override max_connections with a node attribute if DevOps desires.
# For example, on a system *not* dedicated to Postgresql.
if node['postgresql'].attribute?('config_pgtune') && node['postgresql']['config_pgtune'].attribute?('total_memory')
  total_memory = node['postgresql']['config_pgtune']['total_memory']
  if total_memory.match(/\A[1-9]\d*kB\Z/).nil?
    Chef::Application.fatal!([
      "Bad value (#{total_memory})",
      "for node['postgresql']['config_pgtune']['total_memory'] attribute.",
      'Valid values are non-zero integers followed by kB (e.g., 49416564kB).',
    ].join(' '))
  end
end

# Ohai reports node[:memory][:total] in kB, as in "921756kB"
mem = total_memory.split('kB')[0].to_i / 1024 # in MB

#######
# RAM-related settings computed as in Greg Smith's pgtune script.
# Remember that con and mem were either chosen above based on the
# db_type or the actual total memory, or were passed in attributes.

# (1) max_connections
#     Sets the maximum number of concurrent connections.
node.default['postgresql']['config']['max_connections'] = con

# The calculations for the next four settings would not be optimal
# for low memory systems. In that case, the calculation is skipped,
# leaving the built-in Postgresql settings, which are actually
# intended for those low memory systems.
if mem >= 256

  # (2) shared_buffers
  #     Sets the number of shared memory buffers used by the server.
  shared_buffers =
    { 'web' => mem / 4,
      'oltp' => mem / 4,
      'dw' => mem / 4,
      'mixed' => mem / 4,
      'desktop' => mem / 16,
    }.fetch(db_type)

  # Robert Haas has advised to cap the size of shared_buffers based on
  # the memory architecture: 2GB on 32-bit and 8GB on 64-bit machines.
  # http://rhaas.blogspot.com/2012/03/tuning-sharedbuffers-and-walbuffers.html
  case node['kernel']['machine']
  when 'i386' # 32-bit machines
    shared_buffers = 2 * 1024 if shared_buffers > 2 * 1024
  when 'x86_64' # 64-bit machines
    shared_buffers = 8 * 1024 if shared_buffers > 8 * 1024
  end

  node.default['postgresql']['config']['shared_buffers'] = binaryround(shared_buffers * 1024 * 1024)

  # (3) effective_cache_size
  #     Sets the planner's assumption about the size of the disk cache.
  #     That is, the portion of the kernel's disk cache that will be
  #     used for PostgreSQL data files.
  effective_cache_size =
    { 'web' => mem * 3 / 4,
      'oltp' => mem * 3 / 4,
      'dw' => mem * 3 / 4,
      'mixed' => mem * 3 / 4,
      'desktop' => mem / 4,
    }.fetch(db_type)

  node.default['postgresql']['config']['effective_cache_size'] = binaryround(effective_cache_size * 1024 * 1024)

  # (4) work_mem
  #     Sets the maximum memory to be used for query workspaces.
  mem_con_v = (mem.to_f / con).ceil

  work_mem =
    { 'web' => mem_con_v,
      'oltp' => mem_con_v,
      'dw' => mem_con_v / 2,
      'mixed' => mem_con_v / 2,
      'desktop' => mem_con_v / 6,
    }.fetch(db_type)

  node.default['postgresql']['config']['work_mem'] = binaryround(work_mem * 1024 * 1024)

  # (5) maintenance_work_mem
  #     Sets the maximum memory to be used for maintenance operations.
  #     This includes operations such as VACUUM and CREATE INDEX.
  maintenance_work_mem =
    { 'web' => mem / 16,
      'oltp' => mem / 16,
      'dw' => mem / 8,
      'mixed' => mem / 16,
      'desktop' => mem / 16,
    }.fetch(db_type)

  # Cap maintenence RAM at 1GB on servers with lots of memory
  maintenance_work_mem = 1 * 1024 if maintenance_work_mem > 1 * 1024

  node.default['postgresql']['config']['maintenance_work_mem'] = binaryround(maintenance_work_mem * 1024 * 1024)

end

#######
# Checkpoint-related parameters that affect transaction rate and
# maximum tolerable recovery playback time.

# (6) checkpoint_segments
#     Sets the maximum distance in log segments between automatic WAL checkpoints.
checkpoint_segments =
  { 'web' => 8,
    'oltp' => 16,
    'dw' => 64,
    'mixed' => 16,
    'desktop' => 3,
  }.fetch(db_type)

if node['postgresql']['version'].to_f >= 9.5
  node.default['postgresql']['config']['max_wal_size'] = ((3 * checkpoint_segments) * 16).to_s + 'MB'
else
  node.default['postgresql']['config']['checkpoint_segments'] = checkpoint_segments
end

# (7) checkpoint_completion_target
#     Time spent flushing dirty buffers during checkpoint, as fraction
#     of checkpoint interval.
checkpoint_completion_target =
  { 'web' => '0.7',
    'oltp' => '0.9',
    'dw' => '0.9',
    'mixed' => '0.9',
    'desktop' => '0.5',
  }.fetch(db_type)

node.default['postgresql']['config']['checkpoint_completion_target'] = checkpoint_completion_target

# (8) wal_buffers
#     Sets the number of disk-page buffers in shared memory for WAL.
# Starting with 9.1, wal_buffers will auto-tune if set to the -1 default.
# For 8.X and 9.0, it needed to be specified, which pgtune did as follows.
if node['postgresql']['version'].to_f < 9.1
  wal_buffers = 512 * checkpoint_segments
  # The pgtune seems to use 1kB units for wal_buffers
  node.default['postgresql']['config']['wal_buffers'] = binaryround(wal_buffers * 1024)
else
  node.default['postgresql']['config']['wal_buffers'] = '-1'
end

# (9) default_statistics_target
#     Sets the default statistics target. This applies to table columns
#     that have not had a column-specific target set via
#     ALTER TABLE SET STATISTICS.
default_statistics_target =
  { 'web' => 100,
    'oltp' => 100,
    'dw' => 500,
    'mixed' => 100,
    'desktop' => 100,
  }.fetch(db_type)

node.default['postgresql']['config']['default_statistics_target'] = default_statistics_target
