diff --git a/src/include/daos_srv/vos.h b/src/include/daos_srv/vos.h index 96b8204930c..41bf04ad749 100644 --- a/src/include/daos_srv/vos.h +++ b/src/include/daos_srv/vos.h @@ -1100,6 +1100,18 @@ vos_ioh2ci(daos_handle_t ioh); uint32_t vos_ioh2ci_nr(daos_handle_t ioh); +/** + * Get the actual stored epoch of the single value fetched by vos_fetch_begin(). + * Only meaningful for DAOS_IOD_SINGLE akeys after a successful fetch; returns 0 + * if no single value was found (hole, -DER_NONEXIST, or array akey fetch). + * + * \param ioh [IN] The I/O handle. + * + * \return Actual stored epoch, or 0 if no SV was found. + */ +daos_epoch_t +vos_ioh2sv_epoch(daos_handle_t ioh); + /** * Get the scatter/gather list associated with a given I/O descriptor. * diff --git a/src/utils/ddb/ddb.h b/src/utils/ddb/ddb.h index 7107dcc4dc3..fa034ed36a2 100644 --- a/src/utils/ddb/ddb.h +++ b/src/utils/ddb/ddb.h @@ -182,6 +182,12 @@ struct dtx_aggr_options { char *cmt_date; }; +struct csum_dump_options { + char *path; + char *dst; + daos_epoch_t epoch; +}; + /* Run commands ... */ int ddb_run_ls(struct ddb_ctx *ctx, struct ls_options *opt); @@ -240,5 +246,7 @@ int ddb_run_prov_mem(struct ddb_ctx *ctx, struct prov_mem_options *opt); int ddb_run_dtx_aggr(struct ddb_ctx *ctx, struct dtx_aggr_options *opt); +int +ddb_run_csum_dump(struct ddb_ctx *ctx, struct csum_dump_options *opt); #endif /* __DDB_RUN_CMDS_H */ diff --git a/src/utils/ddb/ddb_commands.c b/src/utils/ddb/ddb_commands.c index 669a3eb670b..d29775b4042 100644 --- a/src/utils/ddb/ddb_commands.c +++ b/src/utils/ddb/ddb_commands.c @@ -374,6 +374,314 @@ ddb_run_value_dump(struct ddb_ctx *ctx, struct value_dump_options *opt) return rc; } +static void +print_csum_bufs(struct ddb_ctx *ctx, struct dcs_csum_info *ci) +{ + uint8_t *csum_buf; + int i; + + ddb_print(ctx, "0x"); + csum_buf = ci_idx2csum(ci, 0); + for (i = 0; i < ci->cs_len; i++) + ddb_printf(ctx, "%02" PRIx8, csum_buf[i]); + + for (i = 1; i < ci->cs_nr; i++) { + int j; + + csum_buf = ci_idx2csum(ci, i); + ddb_print(ctx, ", 0x"); + for (j = 0; j < ci->cs_len; j++) + ddb_printf(ctx, "%02" PRIx8, csum_buf[j]); + } + ddb_print(ctx, "\n"); +} + +struct dump_csum_args { + struct ddb_ctx *dca_ctx; + struct dv_indexed_tree_path *dca_vtp; + char *dca_dst_path; +}; + +static int +print_csum_recx(void *cb_args, struct daos_recx_ep_list *recx_rel, daos_epoch_t sv_epoch, + struct dcs_ci_list *cil) +{ + struct dump_csum_args *args; + struct ddb_ctx *ctx; + struct dcs_csum_info *ci; + struct hash_ft *hf; + struct daos_recx_ep *rep; + uint32_t chunk_sz; + int i; + + args = cb_args; + ctx = args->dca_ctx; + + if (cil->dcl_csum_infos_nr == 0) { + ddb_print(ctx, "No checksum at "); + itp_print_full(ctx, args->dca_vtp); + ddb_print(ctx, "\n"); + return 0; + } + + D_ASSERT(recx_rel != NULL); + D_ASSERT(cil->dcl_csum_infos_nr == recx_rel->re_nr); + + ci = dcs_csum_info_get(cil, 0); + D_ASSERT(ci_is_valid(ci)); + rep = &recx_rel->re_items[0]; + hf = daos_mhash_type2algo(ci->cs_type); + chunk_sz = ci->cs_chunksize; + + itp_print_full(ctx, args->dca_vtp); + ddb_print(ctx, "\n"); + ddb_printf(ctx, + "Checksum Type: %s, Checksum Length: %" PRIu16 ", Chunk Size: %" PRIu32 + ", Record Extent(s):\n", + hf->cf_name, ci->cs_len, chunk_sz); + + ddb_printf(ctx, + "- Record Indexes: {%" PRIu64 "-%" PRIu64 "}, Record Size: %" PRIu32 + ", Epoch: %" PRIu64 ", Checksum Value(s): ", + rep->re_recx.rx_idx, rep->re_recx.rx_idx + rep->re_recx.rx_nr - 1, + rep->re_rec_size, rep->re_ep); + print_csum_bufs(ctx, ci); + + for (i = 1; i < cil->dcl_csum_infos_nr; i++) { + ci = dcs_csum_info_get(cil, i); + D_ASSERT(ci_is_valid(ci)); + rep = &recx_rel->re_items[i]; + + ddb_printf(ctx, + "- Record Indexes: {%" PRIu64 "-%" PRIu64 "}, Record Size: %" PRIu32 + ", Epoch: %" PRIu64 ", Checksum Value(s): ", + rep->re_recx.rx_idx, rep->re_recx.rx_idx + rep->re_recx.rx_nr - 1, + rep->re_rec_size, rep->re_ep); + print_csum_bufs(ctx, ci); + } + + return 0; +} + +static int +append_csums2value(d_iov_t *value, struct dcs_csum_info *ci) +{ + uint8_t *buf; + size_t new_len; + int i; + + new_len = value->iov_len + (ci->cs_nr * ci->cs_len); + D_REALLOC(buf, value->iov_buf, value->iov_buf_len, new_len); + if (buf == NULL) + return -DER_NOMEM; + value->iov_buf = buf; + value->iov_buf_len = new_len; + + for (i = 0; i < ci->cs_nr; i++) { + uint8_t *csum_buf = ci_idx2csum(ci, i); + + memcpy((uint8_t *)value->iov_buf + value->iov_len, csum_buf, ci->cs_len); + value->iov_len += ci->cs_len; + } + + return 0; +} + +static int +write_file_csum_recx(void *cb_args, struct daos_recx_ep_list *recx_rel, daos_epoch_t sv_epoch, + struct dcs_ci_list *cil) +{ + struct dump_csum_args *args; + struct ddb_ctx *ctx; + struct dcs_csum_info *ci; + struct hash_ft *hf; + struct daos_recx_ep *rep; + uint32_t chunk_sz; + d_iov_t value = {0}; + int i; + int rc; + + args = cb_args; + ctx = args->dca_ctx; + + if (cil->dcl_csum_infos_nr == 0) { + ddb_print(ctx, "No checksum at "); + itp_print_full(ctx, args->dca_vtp); + ddb_print(ctx, "\n"); + rc = 0; + goto out; + } + + D_ASSERT(recx_rel != NULL); + D_ASSERT(cil->dcl_csum_infos_nr == recx_rel->re_nr); + + ci = dcs_csum_info_get(cil, 0); + D_ASSERT(ci_is_valid(ci)); + rep = &recx_rel->re_items[0]; + hf = daos_mhash_type2algo(ci->cs_type); + chunk_sz = ci->cs_chunksize; + + itp_print_full(ctx, args->dca_vtp); + ddb_print(ctx, "\n"); + ddb_printf(ctx, + "Dumping checksum(s) (Type: %s, Length: %" PRIu16 ", Chunk Size: %" PRIu32 + ") at %s\n", + hf->cf_name, ci->cs_len, chunk_sz, args->dca_dst_path); + + ddb_printf(ctx, + "- Record Indexes: {%" PRIu64 "-%" PRIu64 "}, Record Size: %" PRIu32 + ", Epoch: %" PRIu64 "\n", + rep->re_recx.rx_idx, rep->re_recx.rx_idx + rep->re_recx.rx_nr - 1, + rep->re_rec_size, rep->re_ep); + rc = append_csums2value(&value, ci); + if (!SUCCESS(rc)) + goto out_buf; + + for (i = 1; i < cil->dcl_csum_infos_nr; i++) { + ci = dcs_csum_info_get(cil, i); + D_ASSERT(ci_is_valid(ci)); + rep = &recx_rel->re_items[i]; + + ddb_printf(ctx, + "- Record Indexes: {%" PRIu64 "-%" PRIu64 "}, Record Size: %" PRIu32 + ", Epoch: %" PRIu64 "\n", + rep->re_recx.rx_idx, rep->re_recx.rx_idx + rep->re_recx.rx_nr - 1, + rep->re_rec_size, rep->re_ep); + rc = append_csums2value(&value, ci); + if (!SUCCESS(rc)) + goto out_buf; + } + + rc = ctx->dc_io_ft.ddb_write_file(args->dca_dst_path, &value); + +out_buf: + D_FREE(value.iov_buf); +out: + return rc; +} + +static int +print_csum_sv(void *cb_args, struct daos_recx_ep_list *recx_rel, daos_epoch_t sv_epoch, + struct dcs_ci_list *cil) +{ + struct dump_csum_args *args; + struct ddb_ctx *ctx; + struct dcs_csum_info *ci; + struct hash_ft *hf; + + args = cb_args; + ctx = args->dca_ctx; + + if (cil->dcl_csum_infos_nr == 0) { + ddb_print(ctx, "No checksum at "); + itp_print_full(ctx, args->dca_vtp); + ddb_print(ctx, "\n"); + return 0; + } + D_ASSERT(cil->dcl_csum_infos_nr == 1); + + ci = dcs_csum_info_get(cil, 0); + D_ASSERT(ci_is_valid(ci)); + D_ASSERT(ci->cs_nr == 1); + hf = daos_mhash_type2algo(ci->cs_type); + + itp_print_full(ctx, args->dca_vtp); + ddb_print(ctx, "\n"); + ddb_printf(ctx, "Type: %s, Length: %" PRIu16 ", Epoch: %" PRIu64 ", Value: ", hf->cf_name, + ci->cs_len, sv_epoch); + print_csum_bufs(ctx, ci); + + return 0; +} + +static int +write_file_csum_sv(void *cb_args, struct daos_recx_ep_list *recx_rel, daos_epoch_t sv_epoch, + struct dcs_ci_list *cil) +{ + struct dump_csum_args *args; + struct ddb_ctx *ctx; + struct dcs_csum_info *ci; + struct hash_ft *hf; + d_iov_t value; + int rc; + + args = cb_args; + ctx = args->dca_ctx; + + D_ASSERT(ctx->dc_io_ft.ddb_write_file); + + if (cil->dcl_csum_infos_nr == 0) { + ddb_print(ctx, "No checksum at "); + itp_print_full(ctx, args->dca_vtp); + ddb_print(ctx, "\n"); + rc = 0; + goto out; + } + D_ASSERT(cil->dcl_csum_infos_nr == 1); + + ci = dcs_csum_info_get(cil, 0); + D_ASSERT(ci_is_valid(ci)); + D_ASSERT(ci->cs_nr == 1); + hf = daos_mhash_type2algo(ci->cs_type); + + value.iov_buf = ci_idx2csum(ci, 0); + value.iov_len = ci->cs_len; + value.iov_buf_len = ci->cs_buf_len; + + ddb_printf(ctx, + "Dumping checksum (type: %s, length: %" PRIu16 ", epoch: %" PRIu64 ") to %s\n", + hf->cf_name, ci->cs_len, sv_epoch, args->dca_dst_path); + rc = ctx->dc_io_ft.ddb_write_file(args->dca_dst_path, &value); + +out: + return rc; +} + +int +ddb_run_csum_dump(struct ddb_ctx *ctx, struct csum_dump_options *opt) +{ + struct dv_indexed_tree_path itp = {0}; + struct dv_tree_path vtp; + struct dump_csum_args dca = {0}; + dv_dump_csum_cb cb = NULL; + int rc; + + if (!opt->path) { + ddb_error(ctx, "A VOS path to dump is required.\n"); + rc = -DER_INVAL; + goto out; + } + + rc = init_path(ctx, opt->path, &itp); + if (!SUCCESS(rc)) + goto out; + + if (!itp_has_value(&itp)) { + ddb_errorf(ctx, "Path [%s] is incomplete.\n", opt->path); + rc = -DDBER_INCOMPLETE_PATH_VALUE; + goto out_itp; + } + + if (opt->dst && opt->dst[0] != '\0') + cb = (itp_has_recx(&itp)) ? write_file_csum_recx : write_file_csum_sv; + else + cb = (itp_has_recx(&itp)) ? print_csum_recx : print_csum_sv; + + dca.dca_dst_path = opt->dst; + dca.dca_ctx = ctx; + dca.dca_vtp = &itp; + + itp_to_vos_path(&itp, &vtp); + + rc = dv_dump_csum(ctx->dc_poh, &vtp, opt->epoch, cb, &dca); + +out_itp: + itp_free(&itp); + +out: + return rc; +} + static int dump_ilog_entry_cb(void *cb_arg, struct ddb_ilog_entry *entry) { diff --git a/src/utils/ddb/ddb_vos.c b/src/utils/ddb/ddb_vos.c index 2f748ec9ca3..4536eaa663b 100644 --- a/src/utils/ddb/ddb_vos.c +++ b/src/utils/ddb/ddb_vos.c @@ -1110,8 +1110,7 @@ dump_csum_sv(daos_handle_t coh, daos_key_t *dkey, daos_unit_oid_t *oid, daos_iod } cil = vos_ioh2ci(ioh); - - cb_rc = dump_cb(cb_arg, NULL, cil); + cb_rc = dump_cb(cb_arg, NULL, vos_ioh2sv_epoch(ioh), cil); if (!SUCCESS(cb_rc)) D_DEBUG(DB_IO, "Csum dump callback for " DF_UOID " returned: " DF_RC "\n", DP_UOID(*oid), DP_RC(cb_rc)); @@ -1146,8 +1145,7 @@ dump_csum_recx(daos_handle_t coh, daos_key_t *dkey, daos_unit_oid_t *oid, daos_i cil = vos_ioh2ci(ioh); rel = vos_ioh2recx_list(ioh); - - cb_rc = dump_cb(cb_arg, rel, cil); + cb_rc = dump_cb(cb_arg, rel, 0, cil); if (!SUCCESS(cb_rc)) D_DEBUG(DB_IO, "Csum dump callback for " DF_UOID " returned: " DF_RC "\n", DP_UOID(*oid), DP_RC(cb_rc)); diff --git a/src/utils/ddb/ddb_vos.h b/src/utils/ddb/ddb_vos.h index 7ade0fa462b..d4cda9645c5 100644 --- a/src/utils/ddb/ddb_vos.h +++ b/src/utils/ddb/ddb_vos.h @@ -158,15 +158,18 @@ int dv_dump_value(daos_handle_t poh, struct dv_tree_path *path, dv_dump_value_cb /** * Callback invoked by dv_dump_csum() with the fetched checksum information. * - * @param cb_arg User-provided argument passed through from dv_dump_csum(). - * @param rel Recx/epoch list describing the stored extents. NULL for single-value akeys; - * non-NULL for array akeys. The caller must not free this pointer. - * @param cil Checksum info list. Valid only for the duration of the callback. - * @return 0 on success; a negative error code is propagated back to the caller of - * dv_dump_csum(). + * @param cb_arg User-provided argument passed through from dv_dump_csum(). + * @param recx_rel Recx/epoch list describing the stored extents. Non-NULL for array akeys; + * NULL for single-value akeys. The caller must not free this pointer. + * @param sv_epoch Actual stored epoch of the single value. Non-zero for single-value akeys + * when an SV was found within the requested epoch range; 0 for array akeys + * or when no SV was found (hole or -DER_NONEXIST). + * @param cil Checksum info list. Valid only for the duration of the callback. + * @return 0 on success; a negative error code is propagated back to the caller of + * dv_dump_csum(). */ -typedef int (*dv_dump_csum_cb)(void *cb_arg, struct daos_recx_ep_list *rel, - struct dcs_ci_list *cil); +typedef int (*dv_dump_csum_cb)(void *cb_arg, struct daos_recx_ep_list *recx_rel, + daos_epoch_t sv_epoch, struct dcs_ci_list *cil); /** * Fetch and dump the checksum information for the akey identified by \a path. diff --git a/src/utils/ddb/tests/ddb_commands_tests.c b/src/utils/ddb/tests/ddb_commands_tests.c index 47dc4b7fd98..36cf4e2f5b2 100644 --- a/src/utils/ddb/tests/ddb_commands_tests.c +++ b/src/utils/ddb/tests/ddb_commands_tests.c @@ -586,6 +586,294 @@ dtx_aggr_tests(void **state) "^[[:blank:]]+- Committed DTX count:[[:blank:]]+0$"); } +static void +csum_test_sv_path_init(char *path, size_t path_size, const daos_unit_oid_t *oid, const char *akey) +{ + int rc; + + rc = snprintf(path, path_size, "/%s/" DF_UOID "/%s/%s", g_csum_uuid_str, DP_UOID(*oid), + g_dkeys_str[0], akey); + if (rc < 0 || rc >= path_size) + fail_msg("path buffer too small"); +} + +static void +csum_dump_error_tests(void **state) +{ + char *path_invalid = "foo"; + struct dt_vos_pool_ctx *tctx = *state; + struct ddb_ctx ctx = {0}; + struct csum_dump_options opt = {0}; + int rc; + + ctx.dc_poh = tctx->dvt_poh; + ctx.dc_io_ft.ddb_print_error = dvt_fake_print; + ctx.dc_io_ft.ddb_print_message = dvt_fake_print; + ctx.dc_write_mode = false; + + rc = ddb_run_csum_dump(&ctx, &opt); + assert_rc_equal(-DER_INVAL, rc); + assert_string_contains(dvt_fake_print_buffer, "A VOS path to dump is required."); + dvt_fake_print_reset(); + + opt.path = &path_invalid[0]; + rc = ddb_run_csum_dump(&ctx, &opt); + assert_rc_equal(-DER_INVAL, rc); + assert_string_contains(dvt_fake_print_buffer, "Container is invalid"); + dvt_fake_print_reset(); +} + +static void +csumbuf_dump(char *buf, uint8_t *csumbuf, size_t csumbuf_size) +{ + size_t i; + + for (i = 0; i < csumbuf_size; i++) + buf += sprintf(buf, "%02" PRIx8, csumbuf[i]); + buf[0] = '\0'; +} + +static void +print_csum_sv_tests(void **state) +{ + const char *regex_prf = "0x"; + struct dt_vos_pool_ctx *tctx = *state; + struct dt_csum_ctx *csum_ctx = tctx->dvt_extra; + struct ddb_ctx ctx = {0}; + struct csum_dump_options opt = {0}; + char path[128]; + char buf[256]; + struct dcs_csum_info *ci; + int rc; + + ctx.dc_poh = tctx->dvt_poh; + ctx.dc_io_ft.ddb_print_error = dvt_fake_print; + ctx.dc_io_ft.ddb_print_message = dvt_fake_print; + ctx.dc_write_mode = false; + + opt.path = path; + opt.epoch = DAOS_EPOCH_MAX; + + /* no csum info (g_oids[0]: SV at epoch 1, no checksum stored) */ + csum_test_sv_path_init(path, sizeof(path), &g_oids[0], g_akeys_str[0]); + assert_success(ddb_run_csum_dump(&ctx, &opt)); + rc = snprintf(buf, sizeof(buf), "^No checksum at AKEY:[[:blank:]].+[[:blank:]]%s$", path); + assert_true(rc > 0 && rc < sizeof(buf)); + assert_regex_match(dvt_fake_print_buffer, buf); + dvt_fake_print_reset(); + + /* with csum info, EPOCH_MAX returns the epoch-2 (latest) checksum */ + csum_test_sv_path_init(path, sizeof(path), &g_oids[1], g_akeys_str[0]); + assert_success(ddb_run_csum_dump(&ctx, &opt)); + assert_string_contains(dvt_fake_print_buffer, "Epoch: 2"); + memcpy(buf, regex_prf, strlen(regex_prf)); + ci = csum_ctx->dct_sv_ics[1]->ic_data; + csumbuf_dump(buf + strlen(regex_prf), ci_idx2csum(ci, 0), ci->cs_len); + assert_string_contains(dvt_fake_print_buffer, buf); + dvt_fake_print_reset(); + + /* with csum info, epoch 1 returns the epoch-1 checksum */ + opt.epoch = 1; + assert_success(ddb_run_csum_dump(&ctx, &opt)); + assert_string_contains(dvt_fake_print_buffer, "Epoch: 1"); + memcpy(buf, regex_prf, strlen(regex_prf)); + ci = csum_ctx->dct_sv_ics[0]->ic_data; + csumbuf_dump(buf + strlen(regex_prf), ci_idx2csum(ci, 0), ci->cs_len); + assert_string_contains(dvt_fake_print_buffer, buf); + dvt_fake_print_reset(); +} + +static int +csum_sv_fake_write_file(const char *dst_path, d_iov_t *contents) +{ + struct dcs_csum_info *ci; + + assert_string_equal(dst_path, mock_ptr_type(const char *)); + + ci = mock_ptr_type(struct dcs_csum_info *); + assert_true(contents->iov_len == ci->cs_buf_len); + assert_true(memcmp(contents->iov_buf, ci_idx2csum(ci, 0), ci->cs_buf_len) == 0); + + return mock(); +} + +static void +write_csum_sv_tests(void **state) +{ + char *path_dst = "/tmp/write_csum_sv_test_output.dat"; + struct dt_vos_pool_ctx *tctx = *state; + struct dt_csum_ctx *csum_ctx = tctx->dvt_extra; + struct ddb_ctx ctx = {0}; + struct csum_dump_options opt = {0}; + char path[128]; + char buf[256]; + int rc; + + ctx.dc_poh = tctx->dvt_poh; + ctx.dc_io_ft.ddb_print_error = dvt_fake_print; + ctx.dc_io_ft.ddb_print_message = dvt_fake_print; + ctx.dc_io_ft.ddb_write_file = csum_sv_fake_write_file; + ctx.dc_write_mode = false; + + opt.path = path; + opt.epoch = DAOS_EPOCH_MAX; + opt.dst = path_dst; + + /* no csum info */ + csum_test_sv_path_init(path, sizeof(path), &g_oids[0], g_akeys_str[0]); + assert_success(ddb_run_csum_dump(&ctx, &opt)); + rc = snprintf(buf, sizeof(buf), "^No checksum at AKEY:[[:blank:]].+[[:blank:]]%s$", path); + assert_true(rc > 0 && rc < sizeof(buf)); + assert_regex_match(dvt_fake_print_buffer, buf); + dvt_fake_print_reset(); + + /* with csum info, EPOCH_MAX returns the epoch-2 (latest) checksum */ + csum_test_sv_path_init(path, sizeof(path), &g_oids[1], g_akeys_str[0]); + will_return(csum_sv_fake_write_file, path_dst); + will_return(csum_sv_fake_write_file, csum_ctx->dct_sv_ics[1]->ic_data); + will_return(csum_sv_fake_write_file, 0); + assert_success(ddb_run_csum_dump(&ctx, &opt)); + assert_string_contains(dvt_fake_print_buffer, "Dumping checksum"); + assert_string_contains(dvt_fake_print_buffer, "epoch: 2"); + dvt_fake_print_reset(); + + /* with csum info, epoch 1 returns the epoch-1 checksum */ + opt.epoch = 1; + will_return(csum_sv_fake_write_file, path_dst); + will_return(csum_sv_fake_write_file, csum_ctx->dct_sv_ics[0]->ic_data); + will_return(csum_sv_fake_write_file, 0); + assert_success(ddb_run_csum_dump(&ctx, &opt)); + assert_string_contains(dvt_fake_print_buffer, "Dumping checksum"); + assert_string_contains(dvt_fake_print_buffer, "epoch: 1"); + dvt_fake_print_reset(); +} + +static void +csum_test_recx_path_init(char *path, size_t path_size, const daos_unit_oid_t *oid, const char *akey, + const daos_recx_t *recx) +{ + int rc; + + rc = snprintf(path, path_size, "/%s/" DF_UOID "/%s/%s/{" DF_U64 "-" DF_U64 "}", + g_csum_uuid_str, DP_UOID(*oid), g_dkeys_str[0], akey, recx->rx_idx, + recx->rx_idx + recx->rx_nr - 1); + if (rc < 0 || rc >= path_size) + fail_msg("path buffer too small"); +} + +static void +print_csum_recx_tests(void **state) +{ + const char *regex_prf = "0x"; + struct dt_vos_pool_ctx *tctx = *state; + struct dt_csum_ctx *csum_ctx = tctx->dvt_extra; + struct ddb_ctx ctx = {0}; + struct csum_dump_options opt = {0}; + daos_recx_t recx = {.rx_idx = 0, .rx_nr = csum_ctx->dct_recx_size}; + char path[128]; + char buf[256]; + char *buf_csum; + int i; + int rc; + + ctx.dc_poh = tctx->dvt_poh; + ctx.dc_io_ft.ddb_print_error = dvt_fake_print; + ctx.dc_io_ft.ddb_print_message = dvt_fake_print; + ctx.dc_write_mode = false; + + opt.path = path; + opt.epoch = DAOS_EPOCH_MAX; + + /* no csum info */ + csum_test_recx_path_init(path, sizeof(path), &g_oids[0], g_akeys_str[1], &recx); + assert_success(ddb_run_csum_dump(&ctx, &opt)); + rc = snprintf(buf, sizeof(buf), "^No checksum at RECX:[[:blank:]].+$"); + assert_true(rc > 0 && rc < sizeof(buf)); + assert_regex_match(dvt_fake_print_buffer, buf); + dvt_fake_print_reset(); + + csum_test_recx_path_init(path, sizeof(path), &g_oids[1], g_akeys_str[1], &recx); + assert_success(ddb_run_csum_dump(&ctx, &opt)); + memcpy(buf, regex_prf, strlen(regex_prf)); + buf_csum = buf + strlen(regex_prf); + for (i = 0; i < DVT_FAKE_RECX_COUNT; i++) { + int csum_idx; + struct dcs_csum_info *ci; + + ci = csum_ctx->dct_recx_ics[i]->ic_data; + for (csum_idx = 0; csum_idx < ci->cs_nr; ++csum_idx) { + csumbuf_dump(buf_csum, ci_idx2csum(ci, csum_idx), ci->cs_len); + assert_string_contains(dvt_fake_print_buffer, buf); + } + } + dvt_fake_print_reset(); +} + +static int +csum_recx_fake_write_file(const char *dst_path, d_iov_t *contents) +{ + int i; + uint8_t *buf; + + assert_string_equal(dst_path, mock_ptr_type(const char *)); + + buf = (uint8_t *)contents->iov_buf; + for (i = 0; i < DVT_FAKE_RECX_COUNT; i++) { + int idx; + struct dcs_csum_info *ci; + + ci = mock_ptr_type(struct dcs_csum_info *); + for (idx = 0; idx < ci->cs_nr; ++idx) { + assert_true(memcmp(buf, ci_idx2csum(ci, idx), ci->cs_len) == 0); + buf += ci->cs_len; + } + } + assert_true(buf - (uint8_t *)contents->iov_buf == contents->iov_len); + + return mock(); +} + +static void +write_csum_recx_tests(void **state) +{ + char *path_dst = "/tmp/write_csum_recx_test_output.dat"; + struct dt_vos_pool_ctx *tctx = *state; + struct dt_csum_ctx *csum_ctx = tctx->dvt_extra; + struct ddb_ctx ctx = {0}; + struct csum_dump_options opt = {0}; + daos_recx_t recx = {.rx_idx = 0, .rx_nr = csum_ctx->dct_recx_size}; + char path[128]; + char buf[256]; + int i; + int rc; + + ctx.dc_poh = tctx->dvt_poh; + ctx.dc_io_ft.ddb_print_error = dvt_fake_print; + ctx.dc_io_ft.ddb_print_message = dvt_fake_print; + ctx.dc_io_ft.ddb_write_file = csum_recx_fake_write_file; + ctx.dc_write_mode = false; + + opt.path = path; + opt.epoch = DAOS_EPOCH_MAX; + opt.dst = path_dst; + + csum_test_recx_path_init(path, sizeof(path), &g_oids[0], g_akeys_str[1], &recx); + assert_success(ddb_run_csum_dump(&ctx, &opt)); + rc = snprintf(buf, sizeof(buf), "^No checksum at RECX:[[:blank:]].+$"); + assert_true(rc > 0 && rc < sizeof(buf)); + assert_regex_match(dvt_fake_print_buffer, buf); + dvt_fake_print_reset(); + + csum_test_recx_path_init(path, sizeof(path), &g_oids[1], g_akeys_str[1], &recx); + will_return(csum_recx_fake_write_file, path_dst); + for (i = 0; i < DVT_FAKE_RECX_COUNT; i++) + will_return(csum_recx_fake_write_file, csum_ctx->dct_recx_ics[i]->ic_data); + will_return(csum_recx_fake_write_file, 0); + assert_success(ddb_run_csum_dump(&ctx, &opt)); + assert_string_contains(dvt_fake_print_buffer, "Dumping checksum"); + dvt_fake_print_reset(); +} + /* * -------------------------------------------------------------- * End test functions @@ -635,7 +923,35 @@ dcv_suit_teardown(void **state) return 0; } +static int +dcv_test_csum_setup(void **state) +{ + struct ddb_ctx ctx = {0}; + struct dt_vos_pool_ctx *tctx = *state; + + ctx.dc_write_mode = true; + assert_success(dv_pool_open(tctx->dvt_pmem_file, NULL, &ctx.dc_poh, 0, ctx.dc_write_mode)); + tctx->dvt_poh = ctx.dc_poh; + + assert_success(ddb_test_csum_setup(state)); + + return 0; +} + +static int +dcv_test_csum_teardown(void **state) +{ + struct dt_vos_pool_ctx *tctx = *state; + + ddb_test_csum_teardown(state); + + assert_success(dv_pool_close(tctx->dvt_poh)); + + return 0; +} + #define TEST(test) { #test, test, NULL, NULL } +#define TEST_CSUM(test) {#test, test, dcv_test_csum_setup, dcv_test_csum_teardown} int ddb_commands_tests_run() @@ -658,6 +974,11 @@ ddb_commands_tests_run() TEST(feature_cmd_tests), TEST(open_cmd_tests), TEST(dtx_aggr_tests), + TEST_CSUM(csum_dump_error_tests), + TEST_CSUM(print_csum_sv_tests), + TEST_CSUM(write_csum_sv_tests), + TEST_CSUM(print_csum_recx_tests), + TEST_CSUM(write_csum_recx_tests), }; return cmocka_run_group_tests_name("DDB commands tests", tests, diff --git a/src/utils/ddb/tests/ddb_vos_tests.c b/src/utils/ddb/tests/ddb_vos_tests.c index 63dc15dd323..484876b1fa7 100644 --- a/src/utils/ddb/tests/ddb_vos_tests.c +++ b/src/utils/ddb/tests/ddb_vos_tests.c @@ -1229,7 +1229,8 @@ read_only_vs_write_mode_test(void **state) /* Callback that returns *(int *)cb_args, or 0 if cb_args is NULL. */ static int -csum_cb_return_rc(void *cb_args, struct daos_recx_ep_list *rel, struct dcs_ci_list *cil) +csum_cb_return_rc(void *cb_args, struct daos_recx_ep_list *recx_rel, daos_epoch_t sv_epoch, + struct dcs_ci_list *cil) { return (cb_args != NULL) ? (*(int *)cb_args) : (0); } @@ -1252,11 +1253,17 @@ dump_csum_error_tests(void **state) assert_rc_equal(-DER_INVAL, rc); } +/* + * g_oids[0]: SV stored at epoch 1 without checksum. + * Fetching at EPOCH_MAX finds the SV (sv_epoch=1) but cil is empty (no checksum stored). + */ static int -check_csum_sv_cb_001(void *cb_args, struct daos_recx_ep_list *rel, struct dcs_ci_list *cil) +check_csum_sv_cb_001(void *cb_args, struct daos_recx_ep_list *recx_rel, daos_epoch_t sv_epoch, + struct dcs_ci_list *cil) { assert_null(cb_args); - assert_null(rel); + assert_null(recx_rel); + assert_int_equal(sv_epoch, 1); assert_non_null(cil); assert_int_equal(cil->dcl_csum_infos_nr, 0); @@ -1264,14 +1271,20 @@ check_csum_sv_cb_001(void *cb_args, struct daos_recx_ep_list *rel, struct dcs_ci return 0; } +/* + * g_oids[1]: SV stored at epoch 1 with checksum dct_sv_ics[0]. + * Fetching at epoch=1 returns that exact version. + */ static int -check_csum_sv_cb_002(void *cb_args, struct daos_recx_ep_list *rel, struct dcs_ci_list *cil) +check_csum_sv_cb_002(void *cb_args, struct daos_recx_ep_list *recx_rel, daos_epoch_t sv_epoch, + struct dcs_ci_list *cil) { struct dt_csum_ctx *csum_ctx; struct dcs_csum_info *ci; assert_non_null(cb_args); - assert_null(rel); + assert_null(recx_rel); + assert_int_equal(sv_epoch, 1); assert_non_null(cil); csum_ctx = cb_args; @@ -1290,14 +1303,20 @@ check_csum_sv_cb_002(void *cb_args, struct daos_recx_ep_list *rel, struct dcs_ci return 0; } +/* + * g_oids[1]: SV stored at epoch 2 with checksum dct_sv_ics[1] (latest version). + * Fetching at EPOCH_MAX returns the most recent version. + */ static int -check_csum_sv_cb_003(void *cb_args, struct daos_recx_ep_list *rel, struct dcs_ci_list *cil) +check_csum_sv_cb_003(void *cb_args, struct daos_recx_ep_list *recx_rel, daos_epoch_t sv_epoch, + struct dcs_ci_list *cil) { struct dt_csum_ctx *csum_ctx; struct dcs_csum_info *ci; assert_non_null(cb_args); - assert_null(rel); + assert_null(recx_rel); + assert_int_equal(sv_epoch, 2); assert_non_null(cil); csum_ctx = cb_args; @@ -1353,23 +1372,25 @@ dump_csum_sv_tests(void **state) } static int -check_csum_recx_cb_001(void *cb_args, struct daos_recx_ep_list *rel, struct dcs_ci_list *cil) +check_csum_recx_cb_001(void *cb_args, struct daos_recx_ep_list *recx_rel, daos_epoch_t sv_epoch, + struct dcs_ci_list *cil) { assert_null(cb_args); - assert_non_null(rel); + assert_non_null(recx_rel); + assert_int_equal(sv_epoch, 0); assert_non_null(cil); - assert_int_equal(rel->re_nr, DVT_FAKE_RECX_COUNT); - assert_int_equal(rel->re_items[0].re_recx.rx_idx, 0); - assert_int_equal(rel->re_items[0].re_recx.rx_nr, DVT_FAKE_RECX_SIZE); - assert_int_equal(rel->re_items[0].re_ep, 1); - assert_int_equal(rel->re_items[1].re_recx.rx_idx, DVT_FAKE_RECX_SIZE / 2); + assert_int_equal(recx_rel->re_nr, DVT_FAKE_RECX_COUNT); + assert_int_equal(recx_rel->re_items[0].re_recx.rx_idx, 0); + assert_int_equal(recx_rel->re_items[0].re_recx.rx_nr, DVT_FAKE_RECX_SIZE); + assert_int_equal(recx_rel->re_items[0].re_ep, 1); + assert_int_equal(recx_rel->re_items[1].re_recx.rx_idx, DVT_FAKE_RECX_SIZE / 2); /* * VOS_OF_FETCH_CSUM records the full stored extent, not the IOD intersection: * recx 1 starts at rx_idx=DVT_FAKE_RECX_SIZE/2 but its rx_nr is DVT_FAKE_RECX_SIZE. */ - assert_int_equal(rel->re_items[1].re_recx.rx_nr, DVT_FAKE_RECX_SIZE); - assert_int_equal(rel->re_items[1].re_ep, 2); + assert_int_equal(recx_rel->re_items[1].re_recx.rx_nr, DVT_FAKE_RECX_SIZE); + assert_int_equal(recx_rel->re_items[1].re_ep, 2); /* No csum was stored for g_oids[0], so the checksum info list is empty. */ assert_int_equal(cil->dcl_csum_infos_nr, 0); @@ -1378,13 +1399,15 @@ check_csum_recx_cb_001(void *cb_args, struct daos_recx_ep_list *rel, struct dcs_ } static int -check_csum_recx_cb_002(void *cb_args, struct daos_recx_ep_list *rel, struct dcs_ci_list *cil) +check_csum_recx_cb_002(void *cb_args, struct daos_recx_ep_list *recx_rel, daos_epoch_t sv_epoch, + struct dcs_ci_list *cil) { struct dt_csum_ctx *csum_ctx; struct dcs_csum_info *ci; assert_non_null(cb_args); - assert_non_null(rel); + assert_non_null(recx_rel); + assert_int_equal(sv_epoch, 0); assert_non_null(cil); csum_ctx = cb_args; @@ -1392,13 +1415,13 @@ check_csum_recx_cb_002(void *cb_args, struct daos_recx_ep_list *rel, struct dcs_ assert_int_equal(csum_ctx->dct_chunk_size, DVT_FAKE_CHUNK_SIZE); assert_int_equal(csum_ctx->dct_csum_type, DVT_FAKE_CSUM_TYPE); - assert_int_equal(rel->re_nr, DVT_FAKE_RECX_COUNT); - assert_int_equal(rel->re_items[0].re_recx.rx_idx, 0); - assert_int_equal(rel->re_items[0].re_recx.rx_nr, DVT_FAKE_RECX_SIZE); - assert_int_equal(rel->re_items[0].re_ep, 1); - assert_int_equal(rel->re_items[1].re_recx.rx_idx, DVT_FAKE_RECX_SIZE / 2); - assert_int_equal(rel->re_items[1].re_recx.rx_nr, DVT_FAKE_RECX_SIZE); - assert_int_equal(rel->re_items[1].re_ep, 2); + assert_int_equal(recx_rel->re_nr, DVT_FAKE_RECX_COUNT); + assert_int_equal(recx_rel->re_items[0].re_recx.rx_idx, 0); + assert_int_equal(recx_rel->re_items[0].re_recx.rx_nr, DVT_FAKE_RECX_SIZE); + assert_int_equal(recx_rel->re_items[0].re_ep, 1); + assert_int_equal(recx_rel->re_items[1].re_recx.rx_idx, DVT_FAKE_RECX_SIZE / 2); + assert_int_equal(recx_rel->re_items[1].re_recx.rx_nr, DVT_FAKE_RECX_SIZE); + assert_int_equal(recx_rel->re_items[1].re_ep, 2); assert_non_null(cil); assert_int_equal(cil->dcl_csum_infos_nr, DVT_FAKE_RECX_COUNT); diff --git a/src/vos/tests/vts_io.c b/src/vos/tests/vts_io.c index 340c2869ad9..c8c7fa32b20 100644 --- a/src/vos/tests/vts_io.c +++ b/src/vos/tests/vts_io.c @@ -3066,6 +3066,7 @@ io_csum_fetch_single(void **state) char *update_buf; const size_t update_buf_size = 2 * csum_chunk_size; daos_handle_t ioh; + daos_epoch_t epoch; int rc; arg = *state; @@ -3099,7 +3100,7 @@ io_csum_fetch_single(void **state) assert_rc_equal(rc, 0); /* Write/Update and update mocking counters */ - rc = vos_obj_update(arg->ctx.tc_co_hdl, arg->oid, 1, 0, 0, &dkey, 1, &iod, ic, &sgl); + rc = vos_obj_update(arg->ctx.tc_co_hdl, arg->oid, 42, 0, 0, &dkey, 1, &iod, ic, &sgl); assert_rc_equal(rc, 0); inc_cntr(arg->ta_flags); @@ -3121,6 +3122,8 @@ io_csum_fetch_single(void **state) hf = daos_mhash_type2algo(ci->cs_type); assert_int_equal(hf->cf_type, csum_type); assert_true(daos_csummer_compare_csum_info(csummer, ic->ic_data, ci)); + epoch = vos_ioh2sv_epoch(ioh); + assert_int_equal(epoch, 42); /* Cleanup */ rc = vos_fetch_end(ioh, NULL, rc); @@ -3131,6 +3134,138 @@ io_csum_fetch_single(void **state) D_FREE(update_buf); } +/* + * Write two versions of an SV at distinct epochs and verify that vos_ioh2sv_epoch() returns the + * actual stored epoch (not the requested fetch epoch) for four cases: + * + * 1. EPOCH_MAX resolves to the latest version (epoch 20). + * 2. An intermediate epoch (15) resolves to the nearest earlier version via the B-tree LE probe + * (epoch 10). + * 3. An exact-epoch fetch (10) returns that stored epoch directly. + * 4. An epoch before any write (9) yields an empty result and returns 0 because the key is not + * yet visible and akey_fetch_single() is never reached, leaving ic_sv_epoch at its + * zero-initialised value. + */ +static void +io_csum_sv_epoch(void **state) +{ + const enum DAOS_HASH_TYPE csum_type = HASH_TYPE_CRC64; + const size_t csum_chunk_size = 1u << 12; + + struct io_test_args *arg; + daos_key_t dkey; + daos_key_t akey; + daos_iod_t iod; + d_sg_list_t sgl; + struct dcs_ci_list *cil; + struct daos_csummer *csummer; + struct dcs_iod_csums *ic; + char dkey_name[UPDATE_DKEY_SIZE]; + char akey_name[UPDATE_AKEY_SIZE]; + char *buf; + const size_t buf_size = csum_chunk_size; + daos_handle_t ioh; + int rc; + + arg = *state; + + D_ALLOC(buf, buf_size); + assert_non_null(buf); + + vts_key_gen(&dkey_name[0], arg->dkey_size, true, arg); + set_iov(&dkey, &dkey_name[0], is_daos_obj_type_set(arg->otype, DAOS_OT_DKEY_UINT64)); + vts_key_gen(&akey_name[0], arg->akey_size, false, arg); + set_iov(&akey, &akey_name[0], is_daos_obj_type_set(arg->otype, DAOS_OT_AKEY_UINT64)); + + iod.iod_type = DAOS_IOD_SINGLE; + iod.iod_size = buf_size; + iod.iod_name = akey; + iod.iod_recxs = NULL; + iod.iod_nr = 1; + + rc = daos_csummer_init_with_type(&csummer, csum_type, csum_chunk_size, 0); + assert_rc_equal(rc, 0); + rc = d_sgl_init(&sgl, 1); + assert_rc_equal(rc, 0); + d_iov_set(sgl.sg_iovs, buf, buf_size); + + /* Write version 1 at epoch 10 */ + dts_buf_render(buf, buf_size); + rc = daos_csummer_calc_iods(csummer, &sgl, &iod, NULL, 1, false, NULL, 0, &ic); + assert_rc_equal(rc, 0); + rc = vos_obj_update(arg->ctx.tc_co_hdl, arg->oid, 10, 0, 0, &dkey, 1, &iod, ic, &sgl); + assert_rc_equal(rc, 0); + daos_csummer_free_ic(csummer, &ic); + + /* Write version 2 at epoch 20 */ + dts_buf_render(buf, buf_size); + rc = daos_csummer_calc_iods(csummer, &sgl, &iod, NULL, 1, false, NULL, 0, &ic); + assert_rc_equal(rc, 0); + rc = vos_obj_update(arg->ctx.tc_co_hdl, arg->oid, 20, 0, 0, &dkey, 1, &iod, ic, &sgl); + assert_rc_equal(rc, 0); + daos_csummer_free_ic(csummer, &ic); + + /* + * Case 1: EPOCH_MAX — the LE probe finds the latest stored epoch (20). + */ + iod.iod_size = 0; + rc = vos_fetch_begin(arg->ctx.tc_co_hdl, arg->oid, DAOS_EPOCH_MAX, &dkey, 1, &iod, + VOS_OF_FETCH_CSUM, NULL, &ioh, NULL); + assert_rc_equal(rc, 0); + assert_int_equal(vos_ioh2sv_epoch(ioh), 20); + cil = vos_ioh2ci(ioh); + assert_int_equal(cil->dcl_csum_infos_nr, 1); + rc = vos_fetch_end(ioh, NULL, 0); + assert_rc_equal(rc, 0); + + /* + * Case 2: intermediate epoch (15) — the LE probe resolves to the nearest earlier version, + * which was stored at epoch 10. + */ + iod.iod_size = 0; + rc = vos_fetch_begin(arg->ctx.tc_co_hdl, arg->oid, 15, &dkey, 1, &iod, VOS_OF_FETCH_CSUM, + NULL, &ioh, NULL); + assert_rc_equal(rc, 0); + assert_int_equal(vos_ioh2sv_epoch(ioh), 10); + cil = vos_ioh2ci(ioh); + assert_int_equal(cil->dcl_csum_infos_nr, 1); + rc = vos_fetch_end(ioh, NULL, 0); + assert_rc_equal(rc, 0); + + /* + * Case 3: exact stored epoch (10) — the LE probe lands exactly on the stored epoch; + * vos_ioh2sv_epoch() returns 10. + */ + iod.iod_size = 0; + rc = vos_fetch_begin(arg->ctx.tc_co_hdl, arg->oid, 10, &dkey, 1, &iod, VOS_OF_FETCH_CSUM, + NULL, &ioh, NULL); + assert_rc_equal(rc, 0); + assert_int_equal(vos_ioh2sv_epoch(ioh), 10); + cil = vos_ioh2ci(ioh); + assert_int_equal(cil->dcl_csum_infos_nr, 1); + rc = vos_fetch_end(ioh, NULL, 0); + assert_rc_equal(rc, 0); + + /* + * Case 4: epoch before any write (9) — vos_obj_hold() finds no visible key at epoch 9, + * short-circuits before reaching akey_fetch_single(), and vos_fetch_begin() returns a valid + * ioh with an empty result. vos_ioh2sv_epoch() returns 0 (ic_sv_epoch was never set). + */ + iod.iod_size = 0; + rc = vos_fetch_begin(arg->ctx.tc_co_hdl, arg->oid, 9, &dkey, 1, &iod, VOS_OF_FETCH_CSUM, + NULL, &ioh, NULL); + assert_rc_equal(rc, 0); + assert_int_equal(vos_ioh2sv_epoch(ioh), 0); + cil = vos_ioh2ci(ioh); + assert_int_equal(cil->dcl_csum_infos_nr, 0); + rc = vos_fetch_end(ioh, NULL, 0); + assert_rc_equal(rc, 0); + + daos_csummer_destroy(&csummer); + d_sgl_fini(&sgl, false); + D_FREE(buf); +} + static void io_csum_update_recx(struct io_test_args *arg, daos_epoch_t epoch, daos_key_t *dkey, const daos_key_t *akey, uint64_t recx_idx, size_t recx_size, @@ -3445,6 +3580,8 @@ static const struct CMUnitTest int_tests[] = { {"VOS300.3: Key query negative test", io_query_key_negative, NULL, NULL}, {"VOS300.4: Gang SV update/fetch test", gang_sv_test, NULL, NULL}, {"VOS400.1: Fetch checksum of a single value object", io_csum_fetch_single, NULL, NULL}, + {"VOS400.2: vos_fetch_begin records the stored SV epoch in the fetch handle", io_csum_sv_epoch, + NULL, NULL}, }; static int diff --git a/src/vos/vos_io.c b/src/vos/vos_io.c index bdead35ce7c..ecd76aa1e16 100644 --- a/src/vos/vos_io.c +++ b/src/vos/vos_io.c @@ -32,6 +32,8 @@ struct vos_io_context { /** The epoch bound including uncertainty */ daos_epoch_t ic_bound; daos_epoch_range_t ic_epr; + /** Actual stored epoch of the single value found during fetch; 0 if none was fetched */ + daos_epoch_t ic_sv_epoch; daos_unit_oid_t ic_oid; struct vos_container *ic_cont; daos_iod_t *ic_iods; @@ -962,6 +964,9 @@ akey_fetch_single(daos_handle_t toh, const daos_epoch_range_t *epr, } else if (key.sk_epoch > epr->epr_hi) { /* Uncertainty violation */ D_GOTO(out, rc = -DER_TX_RESTART); + } else { + /* Real SV found within the valid epoch range; record its actual stored epoch. */ + ioc->ic_sv_epoch = key.sk_epoch; } if (ci_is_valid(&csum_info)) @@ -2830,6 +2835,12 @@ vos_ioh2ci_nr(daos_handle_t ioh) return ioc->ic_csum_list.dcl_csum_infos_nr; } +daos_epoch_t +vos_ioh2sv_epoch(daos_handle_t ioh) +{ + return vos_ioh2ioc(ioh)->ic_sv_epoch; +} + struct bio_sglist * vos_iod_sgl_at(daos_handle_t ioh, unsigned int idx) {