STM友好列表作为更改日志

时间:2016-03-22 21:13:36

标签: haskell stm

我需要有关数据结构的建议,以用作原子更改日志。

我试图实现以下算法。有一股传入的流量 更改更新内存映射。在类似Haskell的伪代码中它是

    update :: DataSet -> SomeListOf Change -> Change -> STM (DataSet, SomeListOf Change)
    update dataSet existingChanges newChange = do
      ...
      return (dataSet, existingChanges ++ [newChange])

其中DataSet是一个映射(目前它是来自stm-containers包的Map,https://hackage.haskell.org/package/stm-containers-0.2.10/docs/STMContainers-Map.html)。整个"更新"从任意数量的线程调用。由于域语义,一些Change可能会被拒绝,我使用throwSTM来抛弃事务的影响。如果成功提交了" newChange"被添加到列表中。

存在单独的线程,它调用以下函数:

    flush :: STM (DataSet, SomeListOf Change) -> IO ()

该函数应该将DataSet的当前快照与更改列表(它必须是一致的对)一起取出并将其刷新到文件系统,即

    flush data = do
      (dataSet, changes) <- atomically $ readTVar data_
      -- write them both to FS
      -- ...
      atomically $ writeTVar data_ (dataSet, [])

我需要有关用于&#34; SomeListOf Change&#34;的数据结构的建议。我不想使用[更改],因为它太有序了#34;而且我担心会有太多的冲突,这将迫使整个交易重试。如果我在这里错了,请纠正我。

我无法使用套装(https://hackage.haskell.org/package/stm-containers-0.2.10/docs/STMContainers-Set.html),因为我仍然需要保留某些订单,例如事务提交的顺序。我可以使用TChan它看起来像一个很好的匹配(完全是事务提交的顺序),但我不知道如何实现&#34; flush&#34;函数,以便它与DataSet一起提供整个更改日志的一致视图。

当前的实现是https://github.com/lolepezy/rpki-pub-server/blob/add-storage/src/RRDP/Repo.hs,分别在函数applyActionsToState和rrdpSyncThread中。它使用TChan,似乎以错误的方式做到了。

提前谢谢。

更新:合理的答案似乎就是那样

    type SomeListOf c = TChan [c] 

    update :: DataSet -> TChan [Change] -> Change -> STM DataSet
    update dataSet existingChanges newChange = do
      ...
      writeTChan changeChan $ reverse (newChange : existingChanges)
      return dataSet

   flush data_ = do
      (dataSet, changes) <- atomically $ (,) <$> readTVar data_ <*> readTChan changeChan
      -- write them both to FS
      -- ...

但我仍然不确定它是否是将整个列表作为频道元素传递的简洁解决方案。

2 个答案:

答案 0 :(得分:3)

我可能只是选择列表,看看性能有多远。鉴于此,您应该考虑两者,附加到列表的末尾并将其反转为O(n)操作,因此您应该尽量避免这种情况。也许您可以像这样预先添加传入的更改:

update dataSet existingChanges newChange = do
  -- ...
  return (dataSet, newChange : existingChanges)

另外,您的flush示例存在的问题是,读取和更新状态根本不是原子的。您必须使用单个atomically调用来完成此操作:

flush data = do
  (dataSet, changes) <- atomically $ do
    result <- readTVar data_
    writeTVar data_ (dataSet, [])
    return result

  -- write them both to FS
  -- ...

然后你可以按相反的顺序写出它们(因为现在changes包含从最新到最旧的元素)或者在这里反转一次,如果将它们从最旧到最新写出来是很重要的。如果这很重要,我可能会采用一些允许O(1)元素访问的数据结构,就像一个好的旧向量一样。

当使用固定大小的矢量时,你显然必须处理它可能变得“满”的问题,这意味着你的作家在添加新的变化之前必须等待flush完成它的工作。这就是为什么我个人首先选择简单列表,看看它是否足够或者需要改进的地方。

PS:dequeue也可能适合您的问题,但是固定大小会强制您处理您的作者可能产生的更改比您的读者可以清除更多的问题。出队可以无限增长,但你的RAM可能不是。向量的开销很低。

答案 1 :(得分:0)

我做了一些(非常简单的)调查 https://github.com/lolepezy/rpki-pub-server/tree/add-storage/test/changeLog 正好模仿我应该拥有的负载类型。我为数据集使用了相同的STMContainers.Map,并为更改日志使用了常用列表。为了跟踪事务重试次数,我使用了Debug.Trace.trace,这意味着跟踪打印的行数。由trace打印的唯一行的数量为我提供了已提交事务的数量。

结果在这里(https://github.com/lolepezy/rpki-pub-server/blob/add-storage/test/changeLog/numbers.txt)。第一列是线程数,第二列是总共生成的更改集数。第三列是没有更改日志的情况下的跟踪调用数,最后一列是带有更改日志的跟踪调用数。

显然,大多数时候更改日志会增加一些额外的重试次数,但这几乎是微不足道的。所以,我想,可以说任何数据结构都足够好,因为大多数工作都与更新地图有关,大多数重试都是因为它而发生的。