/**
 * Copyright (c) 2021 OceanBase
 * OceanBase CE is licensed under Mulan PubL v2.
 * You can use this software according to the terms and conditions of the Mulan PubL v2.
 * You may obtain a copy of Mulan PubL v2 at:
 *          http://license.coscl.org.cn/MulanPubL-2.0
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PubL v2 for more details.
 */

#include "ob_clog_writer.h"
#include "storage/ob_partition_service.h"
#include "lib/thread/ob_thread_name.h"
#include "lib/atomic/ob_atomic.h"
#include "lib/allocator/ob_malloc.h"
#include "lib/stat/ob_diagnose_info.h"
#include "lib/statistic_event/ob_stat_event.h"
#include "lib/file/file_directory_utils.h"
#include "share/config/ob_server_config.h"
#include "share/ob_bg_thread_monitor.h"
#include "clog/ob_log_common.h"
#include "clog/ob_log_entry.h"
#include "clog/ob_log_define.h"
#include "clog/ob_log_file_trailer.h"
#include "clog/ob_clog_file_writer.h"
#include "observer/ob_server_struct.h"
#include "election/ob_election.h"

using namespace oceanbase::common;
using namespace oceanbase::share;
using namespace oceanbase::election;

namespace oceanbase {
namespace clog {
ObCLogWriter::ObCLogWriter()
    : is_started_(false),
      is_disk_error_(false),
      is_disk_hang_(false),
      file_mutex_(),
      file_writer_(NULL),
      type_(INVALID_WRITE_POOL),
      log_cache_(0),
      info_getter_(NULL),
      tail_(NULL)
{}

ObCLogWriter::~ObCLogWriter()
{
  destroy();
}

int ObCLogWriter::init(ObCLogWriterCfg& clog_cfg)
{
  int ret = OB_SUCCESS;
  if (OB_UNLIKELY(is_inited())) {
    ret = OB_INIT_TWICE;
    CLOG_LOG(WARN, "The ObCLogWriter has been inited, ", K(ret));
  } else if (OB_UNLIKELY(!clog_cfg.is_valid()) || OB_UNLIKELY(1 != clog_cfg.base_cfg_.group_commit_min_item_cnt_) ||
             OB_UNLIKELY(1 != clog_cfg.base_cfg_.group_commit_max_item_cnt_)) {
    ret = OB_INVALID_ARGUMENT;
    CLOG_LOG(WARN, "Invalid argument, ", K(clog_cfg), K(ret));
  } else if (OB_FAIL(ObBaseLogWriter::init(clog_cfg.base_cfg_))) {
    CLOG_LOG(WARN, "Fail to init base log writer, ", K(ret));
  } else {
    file_writer_ = clog_cfg.log_file_writer_;
    log_cache_ = clog_cfg.log_cache_;
    info_getter_ = clog_cfg.info_getter_;
    tail_ = clog_cfg.tail_ptr_;
    type_ = clog_cfg.type_;
  }

  if (!is_inited()) {
    destroy();
  }
  return ret;
}

int ObCLogWriter::start(file_id_t& file_id, offset_t& offset)
{
  int ret = OB_SUCCESS;
  uint32_t load_file_id = file_id;
  uint32_t load_offset = offset;
  ObAtomicFilePos file_pos;
  if (OB_UNLIKELY(!is_inited())) {
    ret = OB_NOT_INIT;
    CLOG_LOG(ERROR, "The ObCLogWriter has not been inited, ", K(ret));
  } else if (OB_UNLIKELY(is_started_)) {
    ret = OB_INIT_TWICE;
    CLOG_LOG(WARN, "The ObCLogWriter has been started, ", K(ret));
  } else if (!is_valid_file_id(file_id)) {
    ret = OB_INVALID_ARGUMENT;
    CLOG_LOG(WARN, "invalid argument", K(file_id), K(offset));
  } else if (OB_FAIL(file_writer_->load_file(load_file_id, load_offset, false))) {
    CLOG_LOG(WARN, "sync shared memory fail", K(ret), K(file_id), K(offset));
  } else {
    is_started_ = true;
    file_id = load_file_id;
    offset = load_offset;
    CLOG_LOG(INFO, "log writer start success", K(file_id), K(offset));
  }
  return ret;
}

void ObCLogWriter::destroy()
{
  ObBaseLogWriter::destroy();
  type_ = INVALID_WRITE_POOL;
  file_writer_ = NULL;
  log_cache_ = NULL;
  info_getter_ = NULL;
  tail_ = NULL;
  is_disk_error_ = false;
  is_disk_hang_ = false;
  is_started_ = false;
}

int ObCLogWriter::switch_file()
{
  int ret = OB_SUCCESS;
  if (OB_UNLIKELY(!is_started_)) {
    ret = OB_NOT_INIT;
    CLOG_LOG(WARN, "The ObCLogWriter has not been inited, ", K(ret));
  } else {
    lib::ObMutexGuard guard(file_mutex_);
    if (OB_FAIL(inner_switch_file())) {
      CLOG_LOG(WARN, "Fail to switch file, ", K(ret));
    }
  }
  return ret;
}

file_id_t ObCLogWriter::get_file_id() const
{
  return (NULL == file_writer_) ? 0 : file_writer_->get_cur_file_id();
}

bool ObCLogWriter::is_disk_hang() const
{
  bool is_disk_hang = ATOMIC_LOAD(&is_disk_hang_);
  return is_disk_hang;
}

int ObCLogWriter::set_is_disk_hang()
{
  int ret = OB_SUCCESS;
  if (OB_UNLIKELY(!is_inited())) {
    ret = OB_NOT_INIT;
  } else {
    ATOMIC_STORE(&is_disk_hang_, true);
    CLOG_LOG(WARN, "clog disk may be hang or something error has happen!");
  }
  return ret;
}

int ObCLogWriter::reset_is_disk_hang()
{
  int ret = OB_SUCCESS;
  if (OB_UNLIKELY(!is_inited())) {
    ret = OB_NOT_INIT;
  } else {
    ATOMIC_STORE(&is_disk_hang_, false);
    CLOG_LOG(TRACE, "reset clog disk status to normal");
  }
  return ret;
}

void ObCLogWriter::set_clog_writer_thread_name()
{
  static __thread bool has_set_thread_name = false;
  if (!has_set_thread_name) {
    lib::set_thread_name("CLGWR");
    has_set_thread_name = true;
  }
}

void ObCLogWriter::process_log_items(common::ObIBaseLogItem** items, const int64_t item_cnt, int64_t& finish_cnt)
{
  int ret = OB_SUCCESS;
  ObICLogItem* item = NULL;
  int64_t cur_time = 0;
  int64_t io_time = 0;
  int64_t flush_time = 0;
  finish_cnt = 0;
  ObLogBlockMetaV2 block_meta;
  const int64_t block_meta_len = block_meta.get_serialize_size();
  int64_t meta_pos = 0;
  const bool is_disk_error = ATOMIC_LOAD(&is_disk_error_);
  set_clog_writer_thread_name();
  if (OB_UNLIKELY(!is_started_)) {
    ret = OB_NOT_INIT;
    CLOG_LOG(WARN, "The ObCLogWriter has not been started, ", K(ret));
  } else if (OB_UNLIKELY(NULL == items) || OB_UNLIKELY(item_cnt <= 0) || OB_UNLIKELY(item_cnt > 1) ||
             NULL == (item = reinterpret_cast<ObICLogItem*>(items[0])) || OB_UNLIKELY(!item->is_valid()) ||
             OB_UNLIKELY(item->get_data_len() > OB_MAX_LOG_BUFFER_SIZE)) {
    ret = OB_INVALID_ARGUMENT;
    CLOG_LOG(WARN, "Invalid argument, ", K(ret), KP(items), K(item_cnt));
  } else if (OB_UNLIKELY(is_disk_error)) {
    ret = OB_STATE_NOT_MATCH;
    CLOG_LOG(ERROR, "The ObCLogWriter met disk error and been frozen, ", K(ret), K(is_disk_error));
    after_flush(item, block_meta_len, ret, file_writer_->get_cur_file_len(), finish_cnt);
  } else {
    const bool is_idempotent = false;
    const uint64_t write_len = block_meta_len + item->get_data_len();
    const int64_t warning_value = GCONF.data_storage_warning_tolerance_time;
    ObCLogDiskErrorCB* cb = NULL;

    lib::ObMutexGuard guard(file_mutex_);
    BG_NEW_CALLBACK(cb, ObCLogDiskErrorCB, this);
    BG_MONITOR_GUARD(warning_value, is_idempotent, cb);

    // The timestamp value in block header must be generated by the time order, so
    // call inner_switch_file first here.
    if (need_switch_file(write_len) && OB_FAIL(inner_switch_file())) {
      CLOG_LOG(ERROR, "Fail to switch file, ", K(ret), K(file_writer_->get_cur_file_id()));
    } else if (OB_FAIL(block_meta.build_serialized_block(item->get_buf() - block_meta_len,
                   block_meta_len,
                   item->get_buf(),
                   item->get_data_len(),
                   OB_DATA_BLOCK,
                   meta_pos))) {
      CLOG_LOG(ERROR, "build serialized block meta fail", K(ret));
    }

    // invoke callback when fail
    if (OB_FAIL(ret)) {
      after_flush(item, block_meta_len, ret, file_writer_->get_cur_file_len(), finish_cnt);
    }

    if (OB_SUCC(ret)) {
      if (OB_FAIL(file_writer_->append_log_entry(item->get_buf() - block_meta_len, (uint32_t)write_len))) {
        CLOG_LOG(ERROR, "fail to add log item to buf, ", K(ret));
      }
    }

    int64_t flush_start_offset = -1;
    if (OB_SUCC(ret)) {
      cur_time = ObTimeUtility::current_time();
      do {
        // TODO: flush log will not return OB_TIMEOUT, other IO error will be treated as bug
        if (OB_FAIL(file_writer_->flush(info_getter_, log_cache_, tail_, flush_start_offset))) {
          is_disk_error_ = true;
          // flush log to disk until die when IO hang, other IO error will be treated as bug
          if (OB_TIMEOUT == ret && REACH_TIME_INTERVAL(60 * 1000 * 1000)) {
            ret = OB_IO_ERROR;
            CLOG_LOG(ERROR, "flush IO hang, retrying", K(ret));
          } else {
            CLOG_LOG(ERROR, "Fail to flush clog to disk, ", K(ret));
          }
        } else {
          is_disk_error_ = false;
        }
      } while (!has_stoped() && OB_TIMEOUT == ret);
    }

    if (OB_SUCC(ret)) {
      io_time = ObTimeUtility::current_time() - cur_time;
      // log flush succeed, invoke callback when disk sync
      after_flush(item, block_meta_len, ret, flush_start_offset, finish_cnt);
      flush_time = ObTimeUtility::current_time() - cur_time - io_time;

      if (flush_time + io_time > 100 * 1000) {
        CLOG_LOG(WARN,
            "slow flush",
            K(flush_time),
            K(io_time),
            "file_id",
            file_writer_->get_cur_file_id(),
            K(flush_start_offset),
            "file_offset",
            file_writer_->get_cur_file_len());
      }

      if (CLOG_WRITE_POOL == type_) {
        EVENT_INC(CLOG_WRITE_COUNT);
        EVENT_ADD(CLOG_WRITE_TIME, io_time);
      } else if (ILOG_WRITE_POOL == type_) {
        EVENT_INC(ILOG_WRITE_COUNT);
        EVENT_ADD(ILOG_WRITE_TIME, io_time);
      } else {
        CLOG_LOG(ERROR, "unknown write pool type", K(type_));
      }
    }
  }

  if (OB_FAIL(ret)) {
    CLOG_LOG(ERROR, "log writer write data error", K(ret));
    if (!has_stoped()) {
      on_fatal_error(ret);
    }
  }
  if (REACH_TIME_INTERVAL(1000 * 1000)) {
    CLOG_LOG(INFO, "ObCLogWriter get_queued_item_cnt", "queued_cnt", get_queued_item_cnt());
  }
}

bool ObCLogWriter::need_switch_file(const uint64_t write_len) const
{
  // Left space is not enough for data or info block
  uint64_t max_switch_file_limit = 0;
  if (CLOG_WRITE_POOL == type_) {
    max_switch_file_limit = CLOG_MAX_SWITCH_FILE_LIMIT;
  } else {
    max_switch_file_limit = ILOG_MAX_SWITCH_FILE_LIMIT;
  }
  return !file_writer_->enough_file_space(write_len) || (info_getter_->get_entry_cnt() > max_switch_file_limit);
}

void ObCLogWriter::after_flush(ObICLogItem* item, const int64_t block_meta_len, const int err_code,
    const uint32_t file_offset, int64_t& finish_cnt)
{
  int ret = OB_SUCCESS;
  uint32_t file_id = file_writer_->get_cur_file_id();

  if (OB_SUCCESS == err_code) {
    const common::ObAddr addr = GCTX.self_addr_;
    if (OB_FAIL(log_cache_->append_data(addr,
            (char*)(item->get_buf() - block_meta_len),
            file_id,
            file_offset,
            item->get_data_len() + block_meta_len))) {
      CLOG_LOG(ERROR, "Fail to append data to cache, ", K(ret));
    }
    tail_->advance(file_id, file_offset + (uint32_t)item->get_data_len() + (uint32_t)block_meta_len);
  }

  if (OB_FAIL(item->after_flushed(file_id, file_offset + (uint32_t)block_meta_len, err_code, type_))) {
    CLOG_LOG(ERROR, "Fail to process item after flush, ", K(ret));
  }
  ++finish_cnt;
}

int ObCLogWriter::inner_switch_file()
{
  int ret = OB_SUCCESS;
  const int64_t start = ObTimeUtility::current_time();
  if (OB_FAIL(file_writer_->end_current_file(info_getter_, log_cache_, tail_))) {
    CLOG_LOG(ERROR, "switch file failed", K(ret));
  }

  // keep try open new file when too many open files, other IO error will be treated as bug
  if (OB_SUCC(ret)) {
    do {
      if (OB_FAIL(file_writer_->create_next_file())) {
        is_disk_error_ = true;
        if (OB_FILE_NOT_OPENED == ret) {
          if (REACH_TIME_INTERVAL(60 * 1000 * 1000)) {
            CLOG_LOG(ERROR, "Too many open files, retrying", K(ret));
          }
          sleep(30);
        } else {
          CLOG_LOG(ERROR, "Fail to switch file, ", K(ret), K(file_writer_->get_cur_file_id()));
        }
      } else {
        is_disk_error_ = false;
      }
    } while (!has_stoped() && OB_FILE_NOT_OPENED == ret);
  }

  if (OB_SUCC(ret)) {
    if (CLOG_WRITE_POOL == type_) {
      EVENT_ADD(CLOG_FILE_TOTAL_SIZE, CLOG_FILE_SIZE);
    } else if (ILOG_WRITE_POOL == type_) {
      EVENT_ADD(ILOG_FILE_TOTAL_SIZE, CLOG_FILE_SIZE);
    }
    const int64_t end = ObTimeUtility::current_time();
    CLOG_LOG(INFO, "switch file success", K(*tail_), "new_file", file_writer_->get_cur_file_id(), "used", end - start);
  } else {
    CLOG_LOG(ERROR, "switch file error", K(ret), "new_file", file_writer_->get_cur_file_id());
  }

  return ret;
}

ObCLogDiskErrorCB::ObCLogDiskErrorCB(ObCLogWriter* host) : host_(host)
{}

ObCLogDiskErrorCB::~ObCLogDiskErrorCB()
{}

int ObCLogDiskErrorCB::callback()
{
  int ret = OB_SUCCESS;
  storage::ObPartitionService& partition_service = storage::ObPartitionService::get_instance();
  if (OB_ISNULL(host_)) {
    ret = OB_ERR_UNEXPECTED;
  } else if (OB_FAIL(host_->set_is_disk_hang())) {
    CLOG_LOG(ERROR, "ObCLogDiskErrorCB set_is_disk_hang failed", K(ret));
  } else if (OB_FAIL(partition_service.try_revoke_all_leader(ObElection::RevokeType::CLOG_DISK_HANG))) {
    CLOG_LOG(ERROR, "ObCLogDiskErrorCB try_revoke_all_leader failed", K(ret));
  }
  CLOG_LOG(INFO, "call leader revoke because of clog disk hang", K(ret));
  return ret;
}

void ObCLogDiskErrorCB::destroy()
{
  // If disk has real error, may cause is_disk_error be false,
  // however, observer will be killed.
  if (OB_NOT_NULL(host_)) {
    (void)host_->reset_is_disk_hang();
  }
}

int locate_clog_tail(
    const int64_t timeout, ObILogFileStore* file_store, ObLogDirectReader* reader, file_id_t& file_id, offset_t& offset)
{
  ObLogFileTailLocatorImpl<ObLogEntry, ObIRawLogIterator> impl;
  return impl.locate_tail(timeout, file_store, reader, file_id, offset);
}

int locate_ilog_tail(
    const int64_t timeout, ObILogFileStore* file_store, ObLogDirectReader* reader, file_id_t& file_id, offset_t& offset)
{
  ObLogFileTailLocatorImpl<ObIndexEntry, ObIRawIndexIterator> impl;
  return impl.locate_tail(timeout, file_store, reader, file_id, offset);
}

}  // namespace clog
}  // namespace oceanbase
