1module Groonga 2 module CommandLine 3 class Grndb 4 def initialize(argv) 5 @program_path, *@arguments = argv 6 @succeeded = true 7 @database_path = nil 8 end 9 10 def run 11 command_line_parser = create_command_line_parser 12 options = nil 13 begin 14 options = command_line_parser.parse(@arguments) 15 rescue Slop::Error => error 16 $stderr.puts(error.message) 17 $stderr.puts 18 $stderr.puts(command_line_parser.help_message) 19 return false 20 end 21 @succeeded 22 end 23 24 private 25 def create_command_line_parser 26 program_name = File.basename(@program_path) 27 parser = CommandLineParser.new(program_name) 28 29 parser.add_command("check") do |command| 30 command.description = "Check database" 31 32 options = command.options 33 options.banner += " DB_PATH" 34 options.string("--target", "Check only the target object.") 35 36 command.add_action do |options| 37 open_database(command, options) do |database, rest_arguments| 38 check(database, options, rest_arguments) 39 end 40 end 41 end 42 43 parser.add_command("recover") do |command| 44 command.description = "Recover database" 45 46 options = command.options 47 options.banner += " DB_PATH" 48 options.boolean("--force-truncate", "Force to truncate corrupted objects.") 49 50 command.add_action do |options| 51 open_database(command, options) do |database, rest_arguments| 52 recover(database, options, rest_arguments) 53 end 54 end 55 end 56 57 parser 58 end 59 60 def open_database(command, options) 61 arguments = options.arguments 62 if arguments.empty? 63 $stderr.puts("Database path is missing") 64 $stderr.puts 65 $stderr.puts(command.help_message) 66 @succeesed = false 67 return 68 end 69 70 database = nil 71 @database_path, *rest_arguments = arguments 72 begin 73 database = Database.open(@database_path) 74 rescue Error => error 75 $stderr.puts("Failed to open database: <#{@database_path}>") 76 $stderr.puts(error.message) 77 @succeeded = false 78 return 79 end 80 81 begin 82 yield(database, rest_arguments) 83 ensure 84 database.close 85 end 86 end 87 88 def failed(*messages) 89 messages.each do |message| 90 $stderr.puts(message) 91 end 92 @succeeded = false 93 end 94 95 def recover(database, options, arguments) 96 recoverer = Recoverer.new 97 recoverer.database = database 98 recoverer.force_truncate = options[:force_truncate] 99 begin 100 recoverer.recover 101 rescue Error => error 102 failed("Failed to recover database: <#{@database_path}>", 103 error.message) 104 end 105 end 106 107 def check(database, options, arguments) 108 checker = Checker.new 109 checker.program_path = @program_path 110 checker.database_path = @database_path 111 checker.database = database 112 checker.on_failure = lambda do |message| 113 failed(message) 114 end 115 116 checker.check_database 117 118 target_name = options[:target] 119 if target_name 120 checker.check_one(target_name) 121 else 122 checker.check_all 123 end 124 end 125 126 class Checker 127 attr_writer :program_path 128 attr_writer :database_path 129 attr_writer :database 130 attr_writer :on_failure 131 132 def initialize 133 @context = Context.instance 134 @checked = {} 135 end 136 137 def check_database 138 check_database_orphan_inspect 139 check_database_locked 140 check_database_corrupt 141 check_database_dirty 142 end 143 144 def check_one(target_name) 145 target = @context[target_name] 146 if target.nil? 147 exist_p = open_database_cursor do |cursor| 148 cursor.any? do 149 cursor.key == target_name 150 end 151 end 152 if exist_p 153 failed_to_open(target_name) 154 else 155 message = "[#{target_name}] Not exist." 156 failed(message) 157 end 158 return 159 end 160 161 check_object_recursive(target) 162 end 163 164 def check_all 165 open_database_cursor do |cursor| 166 cursor.each do |id| 167 next if ID.builtin?(id) 168 next if builtin_object_name?(cursor.key) 169 next if @context[id] 170 failed_to_open(cursor.key) 171 end 172 end 173 174 @database.each do |object| 175 check_object(object) 176 end 177 end 178 179 private 180 def check_database_orphan_inspect 181 open_database_cursor do |cursor| 182 cursor.each do |id| 183 if cursor.key == "inspect" and @context[id].nil? 184 message = 185 "Database has orphan 'inspect' object. " + 186 "Remove it by '#{@program_path} recover #{@database_path}'." 187 failed(message) 188 break 189 end 190 end 191 end 192 end 193 194 def check_database_locked 195 return unless @database.locked? 196 197 message = 198 "Database is locked. " + 199 "It may be broken. " + 200 "Re-create the database." 201 failed(message) 202 end 203 204 def check_database_corrupt 205 return unless @database.corrupt? 206 207 message = 208 "Database is corrupt. " + 209 "Re-create the database." 210 failed(message) 211 end 212 213 def check_database_dirty 214 return unless @database.dirty? 215 216 last_modified = @database.last_modified 217 if File.stat(@database.path).mtime > last_modified 218 return 219 end 220 221 open_database_cursor do |cursor| 222 cursor.each do |id| 223 next if ID.builtin?(id) 224 path = "%s.%07x" % [@database.path, id] 225 next unless File.exist?(path) 226 return if File.stat(path).mtime > last_modified 227 end 228 end 229 230 message = 231 "Database wasn't closed successfully. " + 232 "It may be broken. " + 233 "Re-create the database." 234 failed(message) 235 end 236 237 def check_object(object) 238 return if @checked.key?(object.id) 239 @checked[object.id] = true 240 241 check_object_locked(object) 242 check_object_corrupt(object) 243 end 244 245 def check_object_locked(object) 246 case object 247 when IndexColumn 248 return unless object.locked? 249 message = 250 "[#{object.name}] Index column is locked. " + 251 "It may be broken. " + 252 "Re-create index by '#{@program_path} recover #{@database_path}'." 253 failed(message) 254 when Column 255 return unless object.locked? 256 name = object.name 257 message = 258 "[#{name}] Data column is locked. " + 259 "It may be broken. " + 260 "(1) Truncate the column (truncate #{name}) or " + 261 "clear lock of the column (lock_clear #{name}) " + 262 "and (2) load data again." 263 failed(message) 264 when Table 265 return unless object.locked? 266 name = object.name 267 message = 268 "[#{name}] Table is locked. " + 269 "It may be broken. " + 270 "(1) Truncate the table (truncate #{name}) or " + 271 "clear lock of the table (lock_clear #{name}) " + 272 "and (2) load data again." 273 failed(message) 274 end 275 end 276 277 def check_object_corrupt(object) 278 case object 279 when IndexColumn 280 return unless object.corrupt? 281 message = 282 "[#{object.name}] Index column is corrupt. " + 283 "Re-create index by '#{@program_path} recover #{@database_path}'." 284 failed(message) 285 when Column 286 return unless object.corrupt? 287 name = object.name 288 message = 289 "[#{name}] Data column is corrupt. " + 290 "(1) Truncate the column (truncate #{name} or " + 291 "'#{@program_path} recover --force-truncate #{@database_path}') " + 292 "and (2) load data again." 293 failed(message) 294 when Table 295 return unless object.corrupt? 296 name = object.name 297 message = 298 "[#{name}] Table is corrupt. " + 299 "(1) Truncate the table (truncate #{name} or " + 300 "'#{@program_path} recover --force-truncate #{@database_path}') " + 301 "and (2) load data again." 302 failed(message) 303 end 304 end 305 306 def check_object_recursive(target) 307 return if @checked.key?(target.id) 308 309 check_object(target) 310 case target 311 when Table 312 unless target.is_a?(Groonga::Array) 313 domain_id = target.domain_id 314 domain = @context[domain_id] 315 if domain.nil? 316 record = Record.new(@database, domain_id) 317 failed_to_open(record.key) 318 elsif domain.is_a?(Table) 319 check_object_recursive(domain) 320 end 321 end 322 323 target.column_ids.each do |column_id| 324 column = @context[column_id] 325 if column.nil? 326 record = Record.new(@database, column_id) 327 failed_to_open(record.key) 328 else 329 check_object_recursive(column) 330 end 331 end 332 when FixedSizeColumn, VariableSizeColumn 333 range_id = target.range_id 334 range = @context[range_id] 335 if range.nil? 336 record = Record.new(@database, range_id) 337 failed_to_open(record.key) 338 elsif range.is_a?(Table) 339 check_object_recursive(range) 340 end 341 342 lexicon_ids = [] 343 target.indexes.each do |index_info| 344 index = index_info.index 345 lexicon_ids << index.domain_id 346 check_object(index) 347 end 348 lexicon_ids.uniq.each do |lexicon_id| 349 lexicon = @context[lexicon_id] 350 if lexicon.nil? 351 record = Record.new(@database, lexicon_id) 352 failed_to_open(record.key) 353 else 354 check_object(lexicon) 355 end 356 end 357 when IndexColumn 358 range_id = target.range_id 359 range = @context[range_id] 360 if range.nil? 361 record = Record.new(@database, range_id) 362 failed_to_open(record.key) 363 return 364 end 365 check_object(range) 366 367 target.source_ids.each do |source_id| 368 source = @context[source_id] 369 if source.nil? 370 record = Record.new(database, source_id) 371 failed_to_open(record.key) 372 elsif source.is_a?(Column) 373 check_object_recursive(source) 374 end 375 end 376 end 377 end 378 379 def open_database_cursor(&block) 380 flags = 381 TableCursorFlags::ASCENDING | 382 TableCursorFlags::BY_ID 383 TableCursor.open(@database, :flags => flags, &block) 384 end 385 386 def builtin_object_name?(name) 387 case name 388 when "inspect" 389 # Just for compatibility. It's needed for users who used 390 # Groonga master at between 2016-02-03 and 2016-02-26. 391 true 392 else 393 false 394 end 395 end 396 397 def failed(message) 398 @on_failure.call(message) 399 end 400 401 def failed_to_open(name) 402 message = 403 "[#{name}] Can't open object. " + 404 "It's broken. " + 405 "Re-create the object or the database." 406 failed(message) 407 end 408 end 409 410 class Recoverer 411 attr_writer :database 412 attr_writer :force_truncate 413 414 def initialize 415 @context = Context.instance 416 end 417 418 def recover 419 if @force_truncate 420 truncate_corrupt_objects 421 end 422 @database.recover 423 end 424 425 def truncate_corrupt_objects 426 @database.each do |object| 427 next unless object.corrupt? 428 logger = @context.logger 429 object_path = object.path 430 object_dirname = File.dirname(object_path) 431 object_basename = File.basename(object_path) 432 object.truncate 433 Dir.foreach(object_dirname) do |path| 434 if path.start_with?("#{object_basename}.") 435 begin 436 File.unlink("#{object_dirname}/#{path}") 437 message = "Corrupted <#{object_path}> related file is removed: <#{path}>" 438 $stdout.puts(message) 439 logger.log(Logger::Level::INFO.to_i, __FILE__, __LINE__, "truncate_corrupt_objects", message) 440 rescue Error => error 441 message = "Failed to remove file which is related to corrupted <#{object_path}>: <#{path}>" 442 $stderr.puts(message) 443 logger.log_error(message) 444 end 445 end 446 end 447 end 448 end 449 end 450 end 451 end 452end 453