Class: Gitlab::Database::LockWritesManager

Inherits:
Object
  • Object
show all
Defined in:
lib/gitlab/database/lock_writes_manager.rb

Constant Summary collapse

TRIGGER_FUNCTION_NAME =
'gitlab_schema_prevent_write'
EXPECTED_TRIGGER_RECORD_COUNT =

Triggers to block INSERT / UPDATE / DELETE Triggers on TRUNCATE are not added to the information_schema.triggers See www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us

3

Instance Method Summary collapse

Constructor Details

#initialize(table_name:, connection:, database_name:, with_retries: true, logger: nil, dry_run: false, force: true) ⇒ LockWritesManager

table_name can include schema name as a prefix. For example: ‘gitlab_partitions_static.events_03’, otherwise, it will default to current used schema, for example ‘public’.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/gitlab/database/lock_writes_manager.rb', line 15

def initialize(
  table_name:, connection:, database_name:,
  with_retries: true, logger: nil, dry_run: false, force: true
)
  @table_name = table_name.to_s
  @connection = connection
  @database_name = database_name
  @logger = logger
  @dry_run = dry_run
  @force = force
  @with_retries = with_retries

  @table_name_without_schema = ActiveRecord::ConnectionAdapters::PostgreSQL::Utils
    .extract_schema_qualified_name(table_name.to_s)
    .identifier
end

Instance Method Details

#lock_writesObject



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/gitlab/database/lock_writes_manager.rb', line 42

def lock_writes
  unless force
    unless table_exist?
      logger&.info "Skipping lock_writes, because #{table_name} does not exist"
      return result_hash(action: 'skipped')
    end

    if table_locked_for_writes?
      logger&.info "Skipping lock_writes, because #{table_name} is already locked for writes"
      return result_hash(action: 'skipped')
    end
  end

  logger&.info Rainbow("Database: '#{database_name}', Table: '#{table_name}': Lock Writes").yellow
  sql_statement = <<~SQL
    CREATE OR REPLACE TRIGGER #{write_trigger_name}
      BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE
      ON #{table_name}
      FOR EACH STATEMENT EXECUTE FUNCTION #{TRIGGER_FUNCTION_NAME}();
  SQL

  result = process_query(sql_statement, 'lock')

  result_hash(action: result)
rescue ActiveRecord::StatementInvalid => e
  # Errors like Gitlab::Database::GitlabSchema::UnknownSchemaError, PG::UndefinedTable
  # are raised under this error
  logger&.warn "Failed lock_writes, because #{table_name} raised an error. Error: #{e}"
  result_hash(action: 'skipped')
end

#table_locked_for_writes?Boolean

Returns:

  • (Boolean)


32
33
34
35
36
37
38
39
40
# File 'lib/gitlab/database/lock_writes_manager.rb', line 32

def table_locked_for_writes?
  query = <<~SQL
      SELECT COUNT(*) from information_schema.triggers
      WHERE event_object_table = '#{table_name_without_schema}'
      AND trigger_name = '#{write_trigger_name}'
  SQL

  connection.select_value(query) == EXPECTED_TRIGGER_RECORD_COUNT
end

#unlock_writesObject



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/gitlab/database/lock_writes_manager.rb', line 73

def unlock_writes
  if force || table_locked_for_writes?
    logger&.info Rainbow("Database: '#{database_name}', Table: '#{table_name}': Allow Writes").green
    sql_statement = <<~SQL
      DROP TRIGGER IF EXISTS #{write_trigger_name} ON #{table_name};
    SQL

    result = process_query(sql_statement, 'unlock')

    result_hash(action: result)
  else
    logger&.info "Skipping unlock_writes, because #{table_name} is already unlocked for writes"
    result_hash(action: 'skipped')
  end
end