Skip to content
This repository has been archived by the owner on Mar 22, 2023. It is now read-only.

Atomic persistent-aware pointer #1209

Merged
merged 1 commit into from
Oct 26, 2021

Conversation

yjrobin
Copy link
Contributor

@yjrobin yjrobin commented Sep 27, 2021

Hi @igchor ,
As we discussed, I created a new PR related to the implementation of atomic_persistent_aware_ptr only.
It contains two different options, read or write-optimized, for this ptr.
I added one UT based on the existing self_relative_ptr_atomic_pmem.
After this is merged, I'll implement a swmr_skip_list based on this with another PR.

Thanks a lot!
Best rgds,
Jun Yang


This change is Reviewable

@codecov
Copy link

codecov bot commented Sep 27, 2021

Codecov Report

Merging #1209 (1bc6e12) into master (2083ad3) will decrease coverage by 2.74%.
The diff coverage is 96.66%.

❗ Current head 1bc6e12 differs from pull request most recent head 704cf5d. Consider uploading reports for the commit 704cf5d to get more accurate results
Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1209      +/-   ##
==========================================
- Coverage   93.31%   90.57%   -2.75%     
==========================================
  Files          52       53       +1     
  Lines        4517     4115     -402     
==========================================
- Hits         4215     3727     -488     
- Misses        302      388      +86     
Flag Coverage Δ
tests_clang_debug_cpp17 ?
tests_gcc_debug 90.57% <96.66%> (+0.37%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
...obj++/experimental/atomic_persistent_aware_ptr.hpp 96.66% <96.66%> (ø)
include/libpmemobj++/utils.hpp 75.00% <0.00%> (-25.00%) ⬇️
include/libpmemobj++/pexceptions.hpp 4.54% <0.00%> (-17.49%) ⬇️
include/libpmemobj++/detail/ringbuf.hpp 79.66% <0.00%> (-17.42%) ⬇️
...lude/libpmemobj++/make_persistent_array_atomic.hpp 85.71% <0.00%> (-14.29%) ⬇️
include/libpmemobj++/experimental/mpsc_queue.hpp 81.40% <0.00%> (-13.97%) ⬇️
include/libpmemobj++/detail/tagged_ptr.hpp 89.39% <0.00%> (-10.61%) ⬇️
...j++/container/detail/concurrent_skip_list_impl.hpp 89.84% <0.00%> (-6.85%) ⬇️
include/libpmemobj++/experimental/radix_tree.hpp 92.51% <0.00%> (-5.63%) ⬇️
include/libpmemobj++/detail/common.hpp 88.88% <0.00%> (-3.71%) ⬇️
... and 36 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2083ad3...704cf5d. Read the comment docs.

Copy link
Member

@lukaszstolarczuk lukaszstolarczuk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 of 3 files at r1, all commit messages.
Reviewable status: 1 of 3 files reviewed, 11 unresolved discussions (waiting on @lukaszstolarczuk and @yjrobin)


-- commits, line 6 at r1:
2 commits with the same description? perhaps they should be just squashed


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 2 at r1 (raw file):

// SPDX-License-Identifier: BSD-3-Clause
/* Copyright 2021-2022, Intel Corporation */

2021 should be enough 😉


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 6 at r1 (raw file):

/**
 * @file
 * Atomic specialization for persistent-ware self_relative_ptr.

misspell - persistent-aware


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 55 at r1 (raw file):

		delete;

	/*

please make these comments a Doxygen style:

/**
 * ...
 */

this way they'll be properly documented.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 84 at r1 (raw file):

	/*
	 * Read-optimized load retries upon dirty ptr, relies on tge store

tge store = misspell


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 99 at r1 (raw file):

		return val;
	}
	/*

pls add an empty line before this comment


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 2 at r1 (raw file):

// SPDX-License-Identifier: BSD-3-Clause
/* Copyright 2020, Intel Corporation */

2021


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 28 at r1 (raw file):

	pmem::obj::experimental::atomic_persistent_aware_ptr<T, ReadOptimized>;

constexpr int ARR_SIZE = 10000;

this test timed out on our Windows CI - perhaps this number is too big...?


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 52 at r1 (raw file):

		});
	} catch (...) {
		UT_ASSERT(0);

please use ASSERT_UNREACHABLE instead of UT_ASSERT(0)


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 83 at r1 (raw file):

test(int argc, char *argv[])
{
	if (argc != 2)

you could check this once and just pass path to the function


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 87 at r1 (raw file):

	char path[100];
	strcpy(path, argv[1]);

you could use strncpy just to be safe (including length of the suffix you add later)

Copy link
Contributor

@igchor igchor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 1 of 3 files reviewed, 17 unresolved discussions (waiting on @igchor, @lukaszstolarczuk, and @yjrobin)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 29 at r1 (raw file):

template <typename T, typename ReadOptimized>
struct atomic_persistent_aware_ptr {

Could you please add some decsription of this pointer? It would be good to mention that it takes care of persisting itself (and also ensuring that data visibility == data persistence).


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 29 at r1 (raw file):

template <typename T, typename ReadOptimized>
struct atomic_persistent_aware_ptr {

Looking at this implementation, I don't see anything pointer-specific. Maybe you could make this structure a generic one (something like template <typename T, typename ReadOptimized> struct atomic_persistent_aware {}'.

and in skip list we would use atomic_persistent_aware<self_relative_ptr<T>> ?


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 94 at r1 (raw file):

		auto val = ptr.load(order);
		while (is_dirty(val)) {
			std::this_thread::yield();

Hm, I'm not sure if yielding right away is the best option, maybe we should try to spin for a while? Please take a look at: https://github.com/pmem/libpmemobj-cpp/blob/master/include/libpmemobj%2B%2B/detail/atomic_backoff.hpp which can be use in this scenario. (It uses pause instructions) Example usage is here: https://github.com/pmem/libpmemobj-cpp/blob/master/include/libpmemobj%2B%2B/container/concurrent_hash_map.hpp#L3049


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 122 at r1 (raw file):

		 std::memory_order order = std::memory_order_seq_cst) noexcept
	{
		return ptr.exchange(desired, order);

I think that all write functions (exchange, compare_exchange_weak/strong, fetch_add, fetch_sub) should use the same mechanism as store for guaranteeing data consistency.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 297 at r1 (raw file):

 */
template <typename T, typename ReadOptimized>
struct can_do_snapshot<

Actually I think we should not allow atomic_persistent_aware_ptr to be snapshotted (that is, it would be better to remove this can_do_snapshot specialization). Unlike self_relative_ptr, this type takes care of persisting itself.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 28 at r1 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

this test timed out on our Windows CI - perhaps this number is too big...?

I belive the Windows timeout might be due to the use of yield. This problem might disappear if you use atomic_backoff as suggested in one of the comments above.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 105 at r1 (raw file):

int
main(int argc, char *argv[])

It would be good to have some test for veryfing data visibility.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 110 at r1 (raw file):

		run_test([&] { test<std::false_type>(argc, argv); });
	auto ret_readopt = run_test([&] { test<std::true_type>(argc, argv); });
	return (ret_writeopt && ret_readopt);

I think we might add a pmreorder test for checking data visibility. The idea would be to run something like this under pmreorder:

void run() {
ptr = nullptr; // (initial value)
ptr.store(0xABCD);
}

and in second application thread:

auto r = ptr.load();
pmem->read = r;

If, on check_consistency test the pmem->read is different from what is actually stored in the ptr this means there is some kind of data inconsistency.

@igchor
Copy link
Contributor

igchor commented Sep 27, 2021

Regarding the pmreorder test, I think our team can try to implment this. However, @yjrobin if you have some other ideas how to implement tests for visibility and presistence feel free to that :)

Copy link
Contributor Author

@yjrobin yjrobin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll think about it and share with you later.

Reviewable status: 1 of 3 files reviewed, 17 unresolved discussions (waiting on @igchor, @lukaszstolarczuk, and @yjrobin)


-- commits, line 6 at r1:

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

2 commits with the same description? perhaps they should be just squashed

We can squash them when merged.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 2 at r1 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

2021 should be enough 😉

Done.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 6 at r1 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

misspell - persistent-aware

Done.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 29 at r1 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

Could you please add some decsription of this pointer? It would be good to mention that it takes care of persisting itself (and also ensuring that data visibility == data persistence).

Done.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 29 at r1 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

Looking at this implementation, I don't see anything pointer-specific. Maybe you could make this structure a generic one (something like template <typename T, typename ReadOptimized> struct atomic_persistent_aware {}'.

and in skip list we would use atomic_persistent_aware<self_relative_ptr<T>> ?

Yes, you are right it is not pointer-specific. Seems like a good idea to make it more generic. It can support any data which is 8-byte aligned and smaller than 8 bytes (to make sure the atomicity of the update and flush).


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 55 at r1 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

please make these comments a Doxygen style:

/**
 * ...
 */

this way they'll be properly documented.

Done.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 84 at r1 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

tge store = misspell

Done.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 94 at r1 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

Hm, I'm not sure if yielding right away is the best option, maybe we should try to spin for a while? Please take a look at: https://github.com/pmem/libpmemobj-cpp/blob/master/include/libpmemobj%2B%2B/detail/atomic_backoff.hpp which can be use in this scenario. (It uses pause instructions) Example usage is here: https://github.com/pmem/libpmemobj-cpp/blob/master/include/libpmemobj%2B%2B/container/concurrent_hash_map.hpp#L3049

Done.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 99 at r1 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

pls add an empty line before this comment

Done.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 122 at r1 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

I think that all write functions (exchange, compare_exchange_weak/strong, fetch_add, fetch_sub) should use the same mechanism as store for guaranteeing data consistency.

Actually, I don't expect this atomic_persistent_aware_ptr to be used in this way. I'll try to implement these functions. The logic of these functions are a bit more complicated than that of store/load. Working on it.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 297 at r1 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

Actually I think we should not allow atomic_persistent_aware_ptr to be snapshotted (that is, it would be better to remove this can_do_snapshot specialization). Unlike self_relative_ptr, this type takes care of persisting itself.

Agree. I'll remove it.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 2 at r1 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

2021

Done.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 28 at r1 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

I belive the Windows timeout might be due to the use of yield. This problem might disappear if you use atomic_backoff as suggested in one of the comments above.

I'll use atomic_backoff to see if the problem disappears.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 52 at r1 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

please use ASSERT_UNREACHABLE instead of UT_ASSERT(0)

Done.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 83 at r1 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

you could check this once and just pass path to the function

Done.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 87 at r1 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

you could use strncpy just to be safe (including length of the suffix you add later)

Done.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 105 at r1 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

It would be good to have some test for veryfing data visibility.

Working on it. If you have any suggestions, please share with me. Thanks.

Copy link
Contributor

@igchor igchor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 1 of 3 files reviewed, 17 unresolved discussions (waiting on @igchor, @lukaszstolarczuk, and @yjrobin)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 29 at r1 (raw file):

Previously, yjrobin wrote…

Yes, you are right it is not pointer-specific. Seems like a good idea to make it more generic. It can support any data which is 8-byte aligned and smaller than 8 bytes (to make sure the atomicity of the update and flush).

Great. Would you like to do this on this PR or it should next step? I think it might be next step because we would probably need to change the DIRTY flag to be MSB instead of LSB (for integer values, LSB could change the value).


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 94 at r1 (raw file):

Previously, yjrobin wrote…

Done.

You should also call pause somewhere at the end of this loop.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 122 at r1 (raw file):

Previously, yjrobin wrote…

Actually, I don't expect this atomic_persistent_aware_ptr to be used in this way. I'll try to implement these functions. The logic of these functions are a bit more complicated than that of store/load. Working on it.

I see, in that case, our first step could be to just remove those functions from the class. I would like for all functions of this atomic to provide the same visibility/persistency rules. What do you think about this?


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 175 at r2 (raw file):

	 */
	bool
	compare_exchange_weak(value_type &expected, value_type desired,

Hm, what if the implementation would just return false whenever CAS fails (including when it's just because someone cleared the dirty flag). The code would be probably easier and from the user perspective, in most case it should not matter why the CAS failed I think - the solution is very often to just try again.

Copy link
Contributor Author

@yjrobin yjrobin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 1 of 3 files reviewed, 17 unresolved discussions (waiting on @igchor, @lukaszstolarczuk, and @yjrobin)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 29 at r1 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

Great. Would you like to do this on this PR or it should next step? I think it might be next step because we would probably need to change the DIRTY flag to be MSB instead of LSB (for integer values, LSB could change the value).

I prefer to do this at next step. The flag bit needs to chosen based on the bit usage of the underlying data, cannot be easily generalized.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 94 at r1 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

You should also call pause somewhere at the end of this loop.

Sorry about missing the most important one :) Done.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 122 at r1 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

I see, in that case, our first step could be to just remove those functions from the class. I would like for all functions of this atomic to provide the same visibility/persistency rules. What do you think about this?

I agree, the 'visibility == persistence' is the key of this atomic ptr.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 175 at r2 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

Hm, what if the implementation would just return false whenever CAS fails (including when it's just because someone cleared the dirty flag). The code would be probably easier and from the user perspective, in most case it should not matter why the CAS failed I think - the solution is very often to just try again.

I think we also need two versions (read/write optimized) for the functions we removed for now. E.g., for read-optimized ptr, store will never clear the dirty flag, so if load is not called, CAS will never succeed. In this case, CAS should take care of the failure when it's due to the dirty flag.
We can discuss later if we decide to add exchange, CAS and fetch to this special atomic ptr.

Copy link
Contributor

@igchor igchor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 1 of 3 files reviewed, 13 unresolved discussions (waiting on @igchor, @lukaszstolarczuk, and @yjrobin)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 175 at r2 (raw file):

Previously, yjrobin wrote…

I think we also need two versions (read/write optimized) for the functions we removed for now. E.g., for read-optimized ptr, store will never clear the dirty flag, so if load is not called, CAS will never succeed. In this case, CAS should take care of the failure when it's due to the dirty flag.
We can discuss later if we decide to add exchange, CAS and fetch to this special atomic ptr.

Agree, let's talk about this later.

Copy link
Member

@lukaszstolarczuk lukaszstolarczuk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 of 2 files at r2, 1 of 1 files at r3, all commit messages.
Reviewable status: all files reviewed, 3 unresolved discussions (waiting on @igchor and @yjrobin)


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 28 at r1 (raw file):

Previously, yjrobin wrote…

I'll use atomic_backoff to see if the problem disappears.

Windows build went just fine, so it looks the issue disappeared.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 87 at r1 (raw file):

Previously, yjrobin wrote…

Done.

I can see you updated strcat, but what about the strcpy? it should copy max 100-5-1=94 chars, I guess (otherwise our static analysis will be throwing error here 😉 )

Copy link
Contributor

@igchor igchor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: all files reviewed, 3 unresolved discussions (waiting on @igchor, @lukaszstolarczuk, and @yjrobin)


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 87 at r1 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

I can see you updated strcat, but what about the strcpy? it should copy max 100-5-1=94 chars, I guess (otherwise our static analysis will be throwing error here 😉 )

You coud also probably just use C++ features: std::string(path) + BoolToString(ReadOptimized::value)

Copy link
Contributor

@KFilipek KFilipek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 of 3 files at r1, 1 of 2 files at r2, 1 of 1 files at r3, all commit messages.
Reviewable status: all files reviewed, 5 unresolved discussions (waiting on @igchor, @lukaszstolarczuk, and @yjrobin)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 36 at r1 (raw file):

		std::atomic<std::ptrdiff_t>>;

	static constexpr uintptr_t IS_DIRTY = 1;

Why uintptr_t? It wasn't designed for "flags".


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 112 at r3 (raw file):

for (detail::atomic_backoff backoff(true);;)

in while(true) we trust:

detail::atomic_backoff backoff(true)
for (;;) {/*code*/}

->

detail::atomic_backoff backoff(true)
while (true) {/*code*/}

Copy link
Contributor Author

@yjrobin yjrobin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 1 of 3 files reviewed, 5 unresolved discussions (waiting on @igchor, @KFilipek, @lukaszstolarczuk, and @yjrobin)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 36 at r1 (raw file):

Previously, KFilipek (Krzysztof Filipek) wrote…

Why uintptr_t? It wasn't designed for "flags".

This flag is used for bitwise operation on pointers (element */void *), so I think it should be fine.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 112 at r3 (raw file):

Previously, KFilipek (Krzysztof Filipek) wrote…
for (detail::atomic_backoff backoff(true);;)

in while(true) we trust:

detail::atomic_backoff backoff(true)
for (;;) {/*code*/}

->

detail::atomic_backoff backoff(true)
while (true) {/*code*/}

Done.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 87 at r1 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

You coud also probably just use C++ features: std::string(path) + BoolToString(ReadOptimized::value)

Done.

Copy link
Contributor

@igchor igchor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 1 of 3 files reviewed, 7 unresolved discussions (waiting on @igchor, @KFilipek, @lukaszstolarczuk, and @yjrobin)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 113 at r4 (raw file):

		if (is_dirty(val)) {
			detail::atomic_backoff backoff(true);
			while (true) {

Actually, with @lukaszstolarczuk we found that this code is not correct. If crash happens during store (after we issued store with dirty flag but before calling compare_exchange_strong to clear the flag), on restart the reader will spin forever (there will be no one to clear the flag). I think the implementation should be just:

auto val = ptr.load(order);
if (is_dirty(val)
    pool_by_vptr(this).persist(&ptr, sizeof(ptr));
return clear_dirty(val);

This will ensure we are returning value which is definitely persisted but avoid infinite loop. The cost of persist should be comparable to spinning anyway imho.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 135 at r4 (raw file):

	{
		auto val = ptr.load(order);
		while (is_dirty(val)) {

I'm not sure if we really need this loop here. Maybe it would be better to just do one iteration, and if compare_exchange_strong fails, we just return clear_dirty(val) and rely on subsequent reader to clear the flag?

Copy link
Member

@lukaszstolarczuk lukaszstolarczuk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 1 of 3 files reviewed, 7 unresolved discussions (waiting on @igchor, @KFilipek, @lukaszstolarczuk, and @yjrobin)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 113 at r4 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

Actually, with @lukaszstolarczuk we found that this code is not correct. If crash happens during store (after we issued store with dirty flag but before calling compare_exchange_strong to clear the flag), on restart the reader will spin forever (there will be no one to clear the flag). I think the implementation should be just:

auto val = ptr.load(order);
if (is_dirty(val)
    pool_by_vptr(this).persist(&ptr, sizeof(ptr));
return clear_dirty(val);

This will ensure we are returning value which is definitely persisted but avoid infinite loop. The cost of persist should be comparable to spinning anyway imho.

I've added the fix in my PR, but feel free to apply it already here - ref. #1217


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 69 at r4 (raw file):

template <typename ReadOptimized>
void
test_ptr_visibility(nvobj::pool<root<ReadOptimized>> &pop)

am I missing something or these tests are exactly the same...?

Copy link
Contributor Author

@yjrobin yjrobin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 1 of 3 files reviewed, 7 unresolved discussions (waiting on @igchor, @KFilipek, @lukaszstolarczuk, and @yjrobin)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 113 at r4 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

I've added the fix in my PR, but feel free to apply it already here - ref. #1217

Good point. To be honest, I have not thought thoroughly about this ReadOptimized version because my original design of this ptr is WriteOptimized only, where the reader is responsible for flushing and clearing the dirty flag to make sure the flush is necessary and executed only once. And it also needs the user's attention (e.g. the swmr-skiplist) to deal with dirty ptrs upon recovery.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 135 at r4 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

I'm not sure if we really need this loop here. Maybe it would be better to just do one iteration, and if compare_exchange_strong fails, we just return clear_dirty(val) and rely on subsequent reader to clear the flag?

If CAS fails, either someone updated it with a new dirty one, or someone flushed it and clear the flag. Just return the clear_dirty(val) may be ok with the later, but it is incorrect with the former case where the returned ptr no longer exists. The while loop can cover both cases.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 105 at r1 (raw file):

Previously, yjrobin wrote…

Working on it. If you have any suggestions, please share with me. Thanks.

I think we can use a simple list to mimic the swmr scenario, and add the recovery process before checking the data visibility/consistency.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 69 at r4 (raw file):

Previously, lukaszstolarczuk (Łukasz Stolarczuk) wrote…

am I missing something or these tests are exactly the same...?

The implementation of test cases is incomplete.

Copy link
Contributor

@igchor igchor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 1 of 3 files reviewed, 6 unresolved discussions (waiting on @igchor, @KFilipek, @lukaszstolarczuk, and @yjrobin)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 113 at r4 (raw file):

Previously, yjrobin wrote…

Good point. To be honest, I have not thought thoroughly about this ReadOptimized version because my original design of this ptr is WriteOptimized only, where the reader is responsible for flushing and clearing the dirty flag to make sure the flush is necessary and executed only once. And it also needs the user's attention (e.g. the swmr-skiplist) to deal with dirty ptrs upon recovery.

Yes, that's true. So, maybe the reader should be implemented in the same way for both ReadOptimized and WriteOptimized cases, we would just change the store() implementation? In general, if store() actually clears the dirty flag it should be rather uncommon for readers to see this dirty flag set.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 135 at r4 (raw file):

Previously, yjrobin wrote…

If CAS fails, either someone updated it with a new dirty one, or someone flushed it and clear the flag. Just return the clear_dirty(val) may be ok with the later, but it is incorrect with the former case where the returned ptr no longer exists. The while loop can cover both cases.

Why it's incorrect? I think it's basically the same as:

ptr.store(0);

thread 1                    | thread 2
auto val = ptr.load(order)  | 
                            | ptr.store(1);
return val; // will return 0|

The first thread will also return value which is no longer valid (but it was at some point in time). Whether the first thread will call compare_exchange_strong before returning should not make any difference I think. Imo we don't violate any ordering rules by returning value which is slightly overrated. The only difference to regular, non-persistent atomic pointers which we must provide is ensuring that what we return is actually persistent.

Copy link
Contributor Author

@yjrobin yjrobin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 1 of 3 files reviewed, 6 unresolved discussions (waiting on @igchor, @KFilipek, @lukaszstolarczuk, and @yjrobin)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 113 at r4 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

Yes, that's true. So, maybe the reader should be implemented in the same way for both ReadOptimized and WriteOptimized cases, we would just change the store() implementation? In general, if store() actually clears the dirty flag it should be rather uncommon for readers to see this dirty flag set.

For correctness, I think we can unify the implementation of load() now. We can evaluate the difference in performance test later.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 135 at r4 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

Why it's incorrect? I think it's basically the same as:

ptr.store(0);

thread 1                    | thread 2
auto val = ptr.load(order)  | 
                            | ptr.store(1);
return val; // will return 0|

The first thread will also return value which is no longer valid (but it was at some point in time). Whether the first thread will call compare_exchange_strong before returning should not make any difference I think. Imo we don't violate any ordering rules by returning value which is slightly overrated. The only difference to regular, non-persistent atomic pointers which we must provide is ensuring that what we return is actually persistent.

I'm thinking a case like follows:

auto val = ptr.load(order); 
while (is_dirty(val)) {
	pool_by_vptr(this).persist(&ptr, sizeof(ptr)); // if the writer update ptr to val2 before the flush here, we will be flushing val2.
	auto clean_val = clear_dirty(val);
	if (ptr.compare_exchange_strong(val, clean_val, order)) // then the CAS will fail, but if we return clear_dirty(val), 
//it will be a problem as we return an obsolete ptr (val) which is not the same as the persistent one (val2)
		return clean_val;
}

The while loop here makes sure we will not return the wrong ptr, if CAS failed, it will assign the value of the current ptr to val and start over again. This extreme case is like there are two consequent updates, before the first one being persistent, the second one has overwritten it. IMO, the first one should not be seen by any readers. What do you think?

Copy link
Contributor

@igchor igchor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 1 of 3 files reviewed, 5 unresolved discussions (waiting on @igchor, @KFilipek, @lukaszstolarczuk, and @yjrobin)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 113 at r4 (raw file):

Previously, yjrobin wrote…

For correctness, I think we can unify the implementation of load() now. We can evaluate the difference in performance test later.

Ok, so let's do that for now. And for store() we can keep the 2 implementations as we have right now.


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 135 at r4 (raw file):

Previously, yjrobin wrote…

I'm thinking a case like follows:

auto val = ptr.load(order); 
while (is_dirty(val)) {
	pool_by_vptr(this).persist(&ptr, sizeof(ptr)); // if the writer update ptr to val2 before the flush here, we will be flushing val2.
	auto clean_val = clear_dirty(val);
	if (ptr.compare_exchange_strong(val, clean_val, order)) // then the CAS will fail, but if we return clear_dirty(val), 
//it will be a problem as we return an obsolete ptr (val) which is not the same as the persistent one (val2)
		return clean_val;
}

The while loop here makes sure we will not return the wrong ptr, if CAS failed, it will assign the value of the current ptr to val and start over again. This extreme case is like there are two consequent updates, before the first one being persistent, the second one has overwritten it. IMO, the first one should not be seen by any readers. What do you think?

My understanding is as follows: modifications performed on the ptr create some sort of linear history, for example like this:

nullptr | val | val2 | // all values before val2 are part of committed 'history'

Now, we could think of this history as a log in a database. What we would like to achieve (I think) is something similar to "read committed" guarantee. This means that we cannot read uncommited (not-persisted) values. In your example, this is not violated because since val was overwritten with val2 and val2 was persisted, val can also be treated as part of this history of committed values.

Do you see any particular cases where this would be problematic?

Copy link
Contributor Author

@yjrobin yjrobin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 1 of 3 files reviewed, 5 unresolved discussions (waiting on @igchor, @KFilipek, and @lukaszstolarczuk)


include/libpmemobj++/experimental/atomic_persistent_aware_ptr.hpp, line 135 at r4 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

My understanding is as follows: modifications performed on the ptr create some sort of linear history, for example like this:

nullptr | val | val2 | // all values before val2 are part of committed 'history'

Now, we could think of this history as a log in a database. What we would like to achieve (I think) is something similar to "read committed" guarantee. This means that we cannot read uncommited (not-persisted) values. In your example, this is not violated because since val was overwritten with val2 and val2 was persisted, val can also be treated as part of this history of committed values.

Do you see any particular cases where this would be problematic?

I understand what you mean. After second thought, I think we can just use "if" here for now. We can discuss this later if we run into problems when we use it to implement SWMR skip-list. I'll merge these changes and push it.


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 105 at r1 (raw file):

Previously, yjrobin wrote…

I think we can use a simple list to mimic the swmr scenario, and add the recovery process before checking the data visibility/consistency.

This will done at #1217

Copy link
Contributor Author

@yjrobin yjrobin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:lgtm:

Reviewable status: 1 of 3 files reviewed, 5 unresolved discussions (waiting on @igchor, @KFilipek, and @lukaszstolarczuk)

Copy link
Contributor

@igchor igchor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yjrobin the code looks good, could you please just fix the test (see the comment for test) and squash all commits into one? After that, we can merge the PR.

Reviewable status: 1 of 3 files reviewed, 3 unresolved discussions (waiting on @KFilipek, @lukaszstolarczuk, and @yjrobin)


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 47 at r5 (raw file):

		nvobj::transaction::run(pop, [&] {
			UT_ASSERT(r->ptr.load() == nullptr);
			r->ptr = nvobj::make_persistent<int>();

The test fails on this line because this assignment does not add r->ptr to a tx. I think the best way to fix this would be to follow a recommendation from this blog post: https://pmem.io/2021/09/17/concurrency.html (part about Publishing allocations), i.e. to create an additional, non-atomic ptr to which we would assign the result of make_persistnt (inside the tx). And then, outside tx, we would call r->ptr.store(non_atomic_ptr); Something like this:

struct root {
    atomic_ptr<int, ReadOptimized> ptr;
    ptr<int> non_atomic_ptr;
};

...

nvobj::transaction::run(pop, [&] {
    ...
    r->non_atomic_ptr = nvobj::make_persistent<int>();
});

r->ptr.store(r->non_atomic_ptr);

...

Copy link
Contributor Author

@yjrobin yjrobin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've fixed the test, please see the comments there. I've also merged the recent commits in pmem:master. If that's ok for you, please merge the PR. Squash commits can be done when merging.

Reviewable status: 1 of 3 files reviewed, 3 unresolved discussions (waiting on @igchor, @KFilipek, and @lukaszstolarczuk)


tests/ptr/atomic_persistent_aware_ptr_pmem.cpp, line 47 at r5 (raw file):

Previously, igchor (Igor Chorążewicz) wrote…

The test fails on this line because this assignment does not add r->ptr to a tx. I think the best way to fix this would be to follow a recommendation from this blog post: https://pmem.io/2021/09/17/concurrency.html (part about Publishing allocations), i.e. to create an additional, non-atomic ptr to which we would assign the result of make_persistnt (inside the tx). And then, outside tx, we would call r->ptr.store(non_atomic_ptr); Something like this:

struct root {
    atomic_ptr<int, ReadOptimized> ptr;
    ptr<int> non_atomic_ptr;
};

...

nvobj::transaction::run(pop, [&] {
    ...
    r->non_atomic_ptr = nvobj::make_persistent<int>();
});

r->ptr.store(r->non_atomic_ptr);

...

Thanks for the tips. I've also modified the pptr deletion accordingly.

Add UT to test atomic_persistent_aware_ptr with both options.
@igchor igchor force-pushed the atomic_persistent_aware_ptr branch from 1bc6e12 to 704cf5d Compare October 26, 2021 14:05
@igchor igchor merged commit f8ea4db into pmem:master Oct 26, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants