RemoveVariable() doesn't fully remove Variable

Created by: germasch

This is going to be a bit convoluted. So looking at the core::IO code, thought I saw a bug in how the variable maps are handled. So I tried to actually trigger it, but I couldn't (immediately).

Instead, I found another bug: When the Variable is removed from the variableMap, the code actually gets a copy of the entire map, then removes the Variable from the copy, and the original map is unchanged. So that's easy to fix:

--- a/source/adios2/core/IO.cpp
+++ b/source/adios2/core/IO.cpp
@@ -165,7 +165,7 @@ bool IO::RemoveVariable(const std::string &name) noexcept
 #define declare_type(T)                                                        \
     else if (type == helper::GetType<T>())                                     \
     {                                                                          \
-        auto variableMap = GetVariableMap<T>();                                \
+        auto& variableMap = GetVariableMap<T>();                                \
         variableMap.erase(index);                                              \
         isRemoved = true;                                                      \
     }

Now, however, a more fundamental problem with these maps appears, that is, they get into inconsistent state when a variable is removed and then another variable is added. Here is a test:

void test2()
{
  using T = int32_t;

  adios2::ADIOS ad("adios.xml", MPI_COMM_WORLD, adios2::DebugON);

  {
    adios2::IO io_writer = ad.DeclareIO("io_writer");
    auto var = io_writer.DefineVariable<T>("var");
    auto var2 = io_writer.DefineVariable<T>("var2");
    auto writer = io_writer.Open("test", adios2::Mode::Write);
    io_writer.RemoveVariable("var");
    auto var3 = io_writer.DefineVariable<T>("var3");
    std::cout << "var2 name " << var2.Name() << std::endl;
    std::cout << "var3 name " << var3.Name() << std::endl;
    
    T test = 99;
    writer.Put(var3, test);
    writer.Close();
  }
}

When run, it prints

var2 name var2
var3 name var2

var3 didn't really get added, instead it became an alias for var2. The problem can be more explicitly seen with this patch:

diff --git a/source/adios2/core/IO.tcc b/source/adios2/core/IO.tcc
index f595748e..447d03f7 100644
--- a/source/adios2/core/IO.tcc
+++ b/source/adios2/core/IO.tcc
@@ -48,6 +48,8 @@ Variable<T> &IO::DefineVariable(const std::string &name, const Dims &shape,
     auto itVariablePair =
         variableMap.emplace(size, Variable<T>(name, shape, start, count,
                                               constantDims, m_DebugMode));
+    bool emplaceSucceeded = itVariablePair.second;
+    if (!emplaceSucceeded) throw std::runtime_error("emplace failed in IO::DefineVariable");
     m_Variables.emplace(name, std::make_pair(helper::GetType<T>(), size));

     Variable<T> &variable = itVariablePair.first->second;

which will make it crash rather than exhibit the incorrect behavior. (Note: There may be a point in always checking the return value from std::map::emplace(), at least in an assert() kind of fashion).

The underlying problem is the one I noted when reading DefineVariable in the first place: The code is assuming that variableMap.size() is going to give it a new, unused index, but that is not the case with a map that can shrink. The easiest solution I can think of is to just maintain a per-map index which gets incremented for each Variable that's inserted, but never decremented. That should be fine as long as it doesn't wrap around.