July 15th, 2022

Processing a ValueSet or PropertySet even in the face of possible mutation, part 4

The customer that was trying to apply the conclusions of this series had a scenario where the PropertySet had both an auto-save feature, as well as an explicit-save feature. So they tried refactoring:

std::optional<SomeKindOfDataBuffer> Widget::SaveToBuffer()
{
    try {
        SomeKindOfDataBuffer buffer;
        auto it = m_propertySet.First();
        if (it.HasCurrent()) {
            do {
                auto current = it.Current();
                buffer.AddKeyAndValue(current.Key(), current.Value());
            } while (it.MoveNext());
        }

        auto guard = m_lock.lock();

        // verify that the collection is still unchanged before returning
        std::ignore = it.HasCurrent();

        return buffer;
    } catch (winrt::hresult_changed_state const&) {
        // Abandon the operation.
        return std::nullopt;
    }
}

void Widget::OnPropertySetChanged()
{
    auto buffer = SaveToBuffer();
    if (buffer) {
        SaveToAutoSaveLocation(*buffer);
    }
}

void Widget::Save()
{
    auto buffer = SaveToBuffer();
    if (!buffer) throw winrt::hresult_changed_state();
    SaveBuffer(*buffer);
    return true;
}

The code to save the property set to a buffer is factored into a helper function, which we then use in the two code paths.

Unfortunately, the refactoring reintroduced the bug.

The purpose of the lock is to prevent a competing auto-save from racing ahead of the current auto-save. But the Save­To­Buffer() releases the lock before returning. The new auto-save code reopens the race condition:

Thread 1 Thread 2
Insert begins  
OnPropertySetChanged begins  
SaveToBuffer begins  
Create the buffer  
Enter the lock  
Check for concurrent mutation  
Release the lock  
Return from SaveToBuffer  
  Insert begins
  OnPropertySetChanged begins
  SaveToBuffer begins
  Create the buffer
  Enter the lock
  Check for concurrent mutation
  Release the lock
  Return from SaveToBuffer
  SaveToAutoSaveLocation
  OnPropertySetChanged returns
  Insert returns
SaveToAutoSaveLocation  
OnPropertySetChanged returns  
Insert returns  

Once you release the lock, another thread can come in and mutate the collection and save the changes. And then you get around to saving the changes, and you overwrote the good changes with out-of-date ones.

Fortunately, we can solve this problem by returning the iterator, too!

std::pair<std::optional<SomeKindOfDataBuffer>,
          winrt::Iterator<winrt::IKeyValuePair<winrt::hstring,
                                               winrt::IInspectable>>>
Widget::SaveToBuffer()
{
    SomeKindOfDataBuffer buffer;
    auto it = m_propertySet.First();
    try {
        if (it.HasCurrent()) {
            do {
                auto current = it.Current();
                buffer.AddKeyAndValue(current.Key(), current.Value());
            } while (it.MoveNext());
        }

        // auto guard = m_lock.lock();

        // verify that the collection is still unchanged before returning
        // std::ignore = it.HasCurrent();

        return { buffer, it };
    } catch (winrt::hresult_changed_state const&) {
        // Abandon the operation.
        return { std::nullopt, it };
    }
}

The two callers can then check the validity of the iterator to determine whether or not to proceed. In the case of auto-save, the check can be performed under the lock.

void Widget::OnPropertySetChanged()
{
    auto [buffer, it] = SaveToBuffer();
    if (buffer) {
        auto guard = m_lock.lock();
        try {
            // final check under the lock
            std::ignore = it.HasCurrent();
            SaveToAutoSaveLocation(*buffer);
        } catch (winrt::hresult_changed_state const&) {
            // Abandon the operation.
        }
    }
}

void Widget::Save()
{
    auto [buffer, it] = SaveToBuffer();

    // Verify that the collection wasn't changed
    // while we were trying to build the buffer.
    std::ignore = it.HasCurrent();

    SaveBuffer(*buffer);
}

An alternative is to make the caller provide the iterator:

std::optional<SomeKindOfDataBuffer>
Widget::SaveToBuffer(
    winrt::Iterator<winrt::IKeyValuePair<winrt::hstring,
                                         winrt::IInspectable>> it)
{
    try {
        SomeKindOfDataBuffer buffer;
        // auto it = m_propertySet.First();
        if (it.HasCurrent()) {
            do {
                auto current = it.Current();
                buffer.AddKeyAndValue(current.Key(), current.Value());
            } while (it.MoveNext());
        }

        // auto guard = m_lock.lock();

        // verify that the collection is still unchanged before returning
        // std::ignore = it.HasCurrent();

        return buffer;
    } catch (winrt::hresult_changed_state const&) {
        // Abandon the operation.
        return std::nullopt;
    }
}

void Widget::OnPropertySetChanged()
{
    auto it = m_propertySet.First();
    auto buffer = SaveToBuffer(it);
    if (buffer) {
        auto guard = m_lock.lock();
        try {
            // final check under the lock
            std::ignore = it.HasCurrent();
            SaveToAutoSaveLocation(*buffer);
        } catch (winrt::hresult_changed_state const&) {
            // Abandon the operation.
        }
    }
}

void Widget::Save()
{
    auto it = m_propertySet.First();
    auto buffer = SaveToBuffer(it);

    // Verify that the collection wasn't changed
    // while we were trying to build the buffer.
    std::ignore = it.HasCurrent();

    SaveBuffer(*buffer);
}

But the pattern I am currently a fan of is to make the caller do the exception handling, too:

SomeKindOfDataBuffer Widget::SaveToBuffer()
{
    SomeKindOfDataBuffer buffer;
    for (auto [name, value] : m_propertySet)
        buffer.AddKeyAndValue(name, value);
    }
    return buffer;
}

void Widget::OnPropertySetChanged() try
{
    auto it = m_propertySet.First();
    auto buffer = SaveToBuffer();

    auto guard = m_lock.lock();
    // final check under the lock
    std::ignore = it.HasCurrent();

    SaveToAutoSaveLocation(buffer);
} catch (winrt::hresult_changed_state const&) {
    // Abandon the operation.
}

void Widget::Save()
{
    auto buffer = SaveToBuffer();
    SaveBuffer(buffer);
}
Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

0 comments

Discussion are closed.