Class: DB

Inherits:
Object
  • Object
show all
Defined in:
backend/app/model/db.rb

Defined Under Namespace

Classes: DBAttempt

Constant Summary

SUPPORTED_DATABASES =
[
 {
   :pattern => /jdbc:mysql/,
   :name => "MySQL"
 },
 {
   :pattern => /jdbc:derby/,
   :name => "Apache Derby"
 }
]

Class Method Summary (collapse)

Class Method Details

+ (Object) after_commit(&block)



105
106
107
108
109
110
111
112
113
# File 'backend/app/model/db.rb', line 105

def self.after_commit(&block)
  if @pool.in_transaction?
    @pool.after_commit do
      block.call
    end
  else
    block.call
  end
end

+ (Object) attempt(&block)



218
219
220
# File 'backend/app/model/db.rb', line 218

def self.attempt(&block)
  DBAttempt.new(block)
end

+ (Object) backups_dir



290
291
292
# File 'backend/app/model/db.rb', line 290

def self.backups_dir
  AppConfig[:backup_directory]
end

+ (Object) blobify(s)



364
365
366
# File 'backend/app/model/db.rb', line 364

def self.blobify(s)
  (@pool.database_type == :derby) ? s.to_sequel_blob : s
end

+ (Object) check_supported(url)



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'backend/app/model/db.rb', line 243

def self.check_supported(url)
  if !SUPPORTED_DATABASES.any? {|db| url =~ db[:pattern]}

    msg = "\n=======================================================================\nUNSUPPORTED DATABASE\n=======================================================================\n\nThe database listed in your configuration:\n\n  \#{url}\n\nis not officially supported by ArchivesSpace.  Although the system may\nstill work, there's no guarantee that future versions will continue to\nwork, or that it will be possible to upgrade without losing your data.\n\nIt is strongly recommended that you run ArchivesSpace against one of\nthese supported databases:\n\n"

    SUPPORTED_DATABASES.each do |db|
      msg += "  * #{db[:name]}\n"
    end

    msg += "\n"
    msg += "\nTo ignore this (very good) advice, you can set the configuration option:\n\n  AppConfig[:allow_unsupported_database] = true\n\n\n=======================================================================\n\n"

    Log.error(msg)

    raise "Database not supported"
  end
end

+ (Object) concat(s1, s2)



369
370
371
372
373
374
375
# File 'backend/app/model/db.rb', line 369

def self.concat(s1, s2)
  if @pool.database_type == :derby
    "#{s1} || #{s2}"
  else
    "CONCAT(#{s1}, #{s2})"
  end
end

+ (Object) connect



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'backend/app/model/db.rb', line 26

def self.connect
  if not @pool

    if !AppConfig[:allow_unsupported_database]
      check_supported(AppConfig[:db_url])
    end

    begin
      Log.info("Connecting to database: #{AppConfig[:db_url_redacted]}. Max connections: #{AppConfig[:db_max_connections]}")
      pool = Sequel.connect(AppConfig[:db_url],
                            :max_connections => AppConfig[:db_max_connections],
                            :test => true,
                            :loggers => (AppConfig[:db_debug_log] ? [Logger.new($stderr)] : [])
                            )

      # Test if any tables exist
      pool[:schema_info].all

      if pool.database_type == :mysql && AppConfig[:allow_non_utf8_mysql_database] != "true"
        ensure_tables_are_utf8(pool)
      end

      @pool = pool
    rescue
      Log.error("DB connection failed: #{$!}")
    end
  end
end

+ (Boolean) connected?

Returns:

  • (Boolean)


93
94
95
# File 'backend/app/model/db.rb', line 93

def self.connected?
  not @pool.nil?
end

+ (Object) demo_db_backup



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'backend/app/model/db.rb', line 318

def self.demo_db_backup
  # Timestamp must come first here for filenames to sort chronologically
  this_backup = File.join(backups_dir, "demo_db_backup_#{Time.now.to_i}_#{$$}")

  Log.info("Writing backup to '#{this_backup}'")

  @pool.pool.hold do |c|
    cs = c.prepare_call("CALL SYSCS_UTIL.SYSCS_BACKUP_DATABASE(?)")
    cs.set_string(1, this_backup.to_s)
    cs.execute
    cs.close
  end

  expire_backups
end

+ (Object) disconnect



238
239
240
# File 'backend/app/model/db.rb', line 238

def self.disconnect
  @pool.disconnect
end

+ (Object) ensure_tables_are_utf8(db)



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'backend/app/model/db.rb', line 56

def self.ensure_tables_are_utf8(db)

  non_utf8_tables = db[:information_schema__tables].
    join(:information_schema__collation_character_set_applicability, :collation_name => :table_collation).
    filter(:table_schema => Sequel.function(:database)).
    filter(Sequel.~(:character_set_name => 'utf8')).all

  unless (non_utf8_tables.empty?)
    msg = "\nThe following MySQL database tables are not set to use UTF-8 for their character\nencoding:\n\n\#{non_utf8_tables.map {|t| \"  * \" + t[:TABLE_NAME]}.join(\"\\n\")}\n\nPlease refer to README.md for instructions on configuring your database to use\nUTF-8.\n\nIf you want to override this restriction (not recommended!) you can set the\nfollowing option in your config.rb file:\n\n  AppConfig[:allow_non_utf8_mysql_database] = \"true\"\n\nBut note that ArchivesSpace largely assumes that your data will be UTF-8\nencoded.  Running in a non-UTF-8 configuration is not supported.\n\n"

    Log.warn(msg)
    raise msg
  end

  Log.info("All tables checked and confirmed set to UTF-8.  Nice job!")
end

+ (Object) expire_backups



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'backend/app/model/db.rb', line 295

def self.expire_backups

  backups = []
  Dir.foreach(backups_dir) do |filename|
    if filename =~ /^demo_db_backup_[0-9]+_[0-9]+$/
      backups << File.join(backups_dir, filename)
    end
  end

  victims = backups.sort.reverse.drop(AppConfig[:demo_db_backup_number_to_keep])

  victims.each do |backup_dir|
    # Proudly paranoid
    if File.exists?(File.join(backup_dir, "archivesspace_demo_db", "BACKUP.HISTORY"))
      Log.info("Expiring old backup: #{backup_dir}")
      FileUtils.rm_rf(backup_dir)
    else
      Log.warn("Too cowardly to delete: #{backup_dir}")
    end
  end
end

+ (Object) increase_lock_version_or_fail(obj)



335
336
337
338
339
340
341
342
343
# File 'backend/app/model/db.rb', line 335

def self.increase_lock_version_or_fail(obj)
  updated_rows = obj.class.dataset.filter(:id => obj.id, :lock_version => obj.lock_version).
                     update(:lock_version => obj.lock_version + 1,
                            :system_mtime => Time.now)

  if updated_rows != 1
    raise Sequel::Plugins::OptimisticLocking::Error.new("Couldn't create version of: #{obj}")
  end
end

+ (Object) is_integrity_violation(exception)

Yeesh.



224
225
226
# File 'backend/app/model/db.rb', line 224

def self.is_integrity_violation(exception)
  (exception.wrapped_exception.cause or exception.wrapped_exception).getSQLState() =~ /^23/
end

+ (Object) is_retriable_exception(exception, opts = {})



229
230
231
232
233
234
235
# File 'backend/app/model/db.rb', line 229

def self.is_retriable_exception(exception, opts = {})
  # Transaction was rolled back, but we can retry
  (exception.instance_of?(RetryTransaction) ||
   (opts[:retry_on_optimistic_locking_fail] &&
    exception.instance_of?(Sequel::Plugins::OptimisticLocking::Error)) ||
   (exception.wrapped_exception.cause or exception.wrapped_exception).getSQLState() =~ /^(40|41)/)
end

+ (Object) jdbc_metadata



173
174
175
176
177
# File 'backend/app/model/db.rb', line 173

def self.
  md =  open { |p|  p.synchronize { |c| c. }} 
  { "databaseProductName" => md.getDatabaseProductName, 
    "databaseProductVersion" => md.getDatabaseProductVersion } 
end

+ (Boolean) needs_blob_hack?

Returns:

  • (Boolean)


360
361
362
# File 'backend/app/model/db.rb', line 360

def self.needs_blob_hack?
  (@pool.database_type == :derby)
end

+ (Boolean) needs_savepoint?

Returns:

  • (Boolean)


184
185
186
187
188
189
# File 'backend/app/model/db.rb', line 184

def self.needs_savepoint?
  # Postgres needs a savepoint for any statement that might fail
  # (otherwise the whole transaction becomes invalid).  Use a savepoint to
  # run the happy case, since we're half expecting it to fail.
  [:postgres].include?(@pool.database_type)
end

+ (Object) open(transaction = true, opts = {})



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'backend/app/model/db.rb', line 116

def self.open(transaction = true, opts = {})
  last_err = false
  retries = opts[:retries] || 10

  retries.times do |attempt|
    begin
      if transaction
        self.transaction do
          return yield @pool
        end

        # Sometimes we'll make it to here.  That means we threw a
        # Sequel::Rollback which has been quietly caught.
        return nil
      else
        begin
          return yield @pool
        rescue Sequel::Rollback
          # If we're not in a transaction we can't roll back, but no need to blow up.
          Log.warn("Sequel::Rollback caught but we're not inside of a transaction")
          return nil
        end
      end


    rescue Sequel::DatabaseDisconnectError => e
      # MySQL might have been restarted.
      last_err = e
      Log.info("Connecting to the database failed.  Retrying...")
      sleep(opts[:db_failed_retry_delay] || 3)


    rescue Sequel::DatabaseError => e
      if (attempt + 1) < retries && is_retriable_exception(e, opts) && transaction
        Log.info("Retrying transaction after retriable exception (#{e})")
        sleep(opts[:retry_delay] || 1)
      else
        raise e
      end
    end

  end

  if last_err
    Log.error("Failed to connect to the database")
    Log.exception(last_err)

    raise "Failed to connect to the database: #{last_err}"
  end
end

+ (Boolean) supports_jasper?

Returns:

  • (Boolean)


345
346
347
# File 'backend/app/model/db.rb', line 345

def self.supports_jasper?
  ![:derby, :h2].include?(@pool.database_type)
end

+ (Boolean) supports_join_updates?

Returns:

  • (Boolean)


355
356
357
# File 'backend/app/model/db.rb', line 355

def self.supports_join_updates?
  ![:derby, :h2].include?(@pool.database_type)
end

+ (Boolean) supports_mvcc?

Returns:

  • (Boolean)


350
351
352
# File 'backend/app/model/db.rb', line 350

def self.supports_mvcc?
  ![:derby, :h2].include?(@pool.database_type)
end

+ (Object) sysinfo



168
169
170
# File 'backend/app/model/db.rb', line 168

def self.sysinfo
  .merge() 
end

+ (Object) system_metadata



179
180
181
182
# File 'backend/app/model/db.rb', line 179

def self.
  RbConfig.const_get("CONFIG").select { |key| ['host_os', 'host_cpu', 
                                                'build', 'ruby_version'].include? key }
end

+ (Object) transaction(*args)



98
99
100
101
102
# File 'backend/app/model/db.rb', line 98

def self.transaction(*args)
  @pool.transaction(*args) do
    yield
  end
end