Packages

A library for distributed reactive programming with flexible consistency guarantees drawing from QUARP and Rx.. Features the familiar behaviours and event streams in the spirit of FRP.

Current section

Files

Jump to
bquarp lib reactivity quality context.ex
Raw

lib/reactivity/quality/context.ex

defmodule Reactivity.Quality.Context do
@moduledoc """
Essential operations on message contexts.
For every guarantee, the functions combine, penalty and transform need to be implemented.
Guarantees with obvious context-progression (e.g. with counters) should implement new_context_obs.
"""
alias Observables.Obs
alias Reactivity.Quality.Guarantee
require Logger
################################
###### COMBINING CONTEXTS ######
# (INTO INTERMEDIATE CONTEXTS) #
################################
@doc """
Combines a list of contexts in the case of enforcing fifo with propagate semantics
Takes a list of fifo contexts:
[nil]
Returns an intermediate fifo context (which is also a finalized fifo-context
by virtue of the transformation for fifo being the identity):
nil
"""
def combine(contexts, {:fp, _}), do: combine(contexts, :fp)
def combine(_contexts, :fp), do: nil
@doc """
Combines a list of contexts in the case of enforcing fifo with update semantics
Takes a list of fifo contexts: [nil]
Returns a fifo context: nil
"""
def combine(contexts, {:fu, _}), do: combine(contexts, :fu)
def combine(_contexts, :fu), do: nil
@doc """
combines a list of contexts in the case of enforcing time synchronization (clock-difference)
Takes a list of time-synchronization contexts:
[stamp(s)] with stamp(s) = stamp | {lowest_stamp, highest_stamp}
Returns an intermediate time-synchronization context
(which is also a finalized time-synchronization context
by virtue of the transformation for time-synch being the identity):
stamp(s)
The lowest and highest timestamp get recalculated from all the stamp(s)s in the list.
[{4, 5}, 2, 3, {3, 6}] -> {2, 6}
"""
def combine(contexts, {:t, _}), do: combine(contexts, :t)
def combine(contexts, :t) do
lows = contexts |> Stream.map(fn
{low, _high} -> low
time -> time
end)
highs = contexts |> Stream.map(fn
{_low, high} -> high
time -> time
end)
{low, high} = {Enum.min(lows), Enum.max(highs)}
if (low == high), do: low, else: {low, high}
end
@doc """
combines a list of contexts in the case of enforcing glitch freedom
Takes a list of glitch-freedom contexts
[[{source, counter(s)}]] with counter(s) = counter | {lowest_counter, highest_counter}
Returns a an intermediate glitch-freedom context
(which is also a finalized glitch-freedom context
by virtue of the transformation for glitch-freedom being the identity):
[{source, counter(s)}]
For each source, the lowest and highest counter gets recalculated from all the tuples {source, counter(s)} with that source.
If those are the same, only one counter value is present.
E.g.:
[{:a, 5}, {:a, 4}, {:b, 11}, {:c, {1, 2}}, {:c, 3}] -> [{:a, {4, 5}}, {:b, 11}, {:c, {1, 3}}]
"""
def combine(contexts, {:g, _}), do: combine(contexts, :g)
def combine(contexts, :g) do
contexts
|> List.flatten
|> Enum.group_by(&(elem(&1,0)))
|> Map.values
|> Enum.map(fn
[h | []] -> h
slst ->
lows = slst |> Stream.map(fn
{_, {low, _}} -> low
{_, counter} -> counter
end)
highs = slst |> Stream.map(fn
{_, {_, high}} -> high
{_, counter} -> counter
end)
s = slst |> List.first |> elem(0)
{low, high} = {Enum.min(lows), Enum.max(highs)}
c = if (low == high), do: low, else: {low, high}
{s, c}
end)
end
@doc """
Combines a list of contexts in the case of enforcing causality
Takes a list of causality contexts:
[c] (with c = path = [tree | [{node, counter}]] | [{node, counter}])
Returns an intermediate causality context: ic = [c]
"""
def combine(contexts, {:c, _}), do: combine(contexts, :c)
def combine(contexts, :c), do: contexts
@doc """
Combines multiple lists of contexts of possibly different consistency guarantees by combining
the contexts pertaining to each occurring guarantee type separately.
Takes
* a list of context lists [[c]]]
* a list of guarantee lists [[g]]]
Returns a list of contexts [c]
"""
def combine(contextss, guaranteess) do
guaranteess
|> Enum.zip(contextss)
|> Enum.map(fn {guarantees, contexts} ->
Enum.zip(guarantees, contexts) end)
|> List.flatten
|> Enum.group_by(fn {{cgt, _cgm}, _ac} -> cgt end)
|> Map.values
|> Stream.map(fn lst -> Enum.unzip(lst) end)
|> Enum.map(fn {gs, cs} ->
[g] = Guarantee.combine(gs)
c = combine(cs, g)
c end)
end
##############################################
# DETERMINING (INTERMEDIATE) CONTEXT QUALITY #
##############################################
@doc """
Decides whether the given list of contexts accompanied by their respective guarantees
is acceptable under these guarantees.
Returns true or false
The given list of contexts is of sufficient qualityif for every context, it satisfies the accompanying guarantee.
"""
def sufficient_quality?([], _gs), do: true
def sufficient_quality?([ch | ct], [gh | gt]) do
sufficient_quality?(ch, gh) and sufficient_quality?(ct, gt)
end
@doc """
Decides whether a given context is acceptable under the given consistency guarantee.
"""
def sufficient_quality?(context, {cgt, cgm}) do
penalty(context, cgt) <= cgm
end
#############################################
@doc """
Calculates the penalty of a context in the case of enforcing fifo with propagate semantics
"""
def penalty(context, {:fp, _}), do: penalty(context, :fp)
def penalty(_context, :fp), do: 0
@doc """
Calculates the penalty of a context in the case of enforcing fifo with update semantics
"""
def penalty(context, {:fu, _}), do: penalty(context, :fu)
def penalty(_context, :fu), do: 0
@doc """
calculates the penalty of a context in the case of enforcing time-synchronization.
Takes a context of the form t or {t_low, t_high} and calculates the difference.
"""
def penalty(context, {:t, _}), do: penalty(context, :t)
def penalty({low, high}, :t), do: high-low
def penalty(_time, :t), do: 0
@doc """
calculates the penalty of a context in the case of enforcing glitch freedom
Takes a context of the form [{si, ci}] with si = counter_i | {lowest_counter_i, highest_counter_i}
And returns the maximum difference between counters attached to the same source.
"""
def penalty(context, {:g, _}), do: penalty(context, :g)
def penalty(context, :g) do
context
|> Stream.map(fn
{_s, {low, high}} -> high-low
{_s, _counter} -> 0
end)
|> Enum.max
end
@doc """
calculates the penalty of a context in the case of enforcing causality.
Compares paths/trees in the context two by two and takes the maximum penalty from all comparisons
Two paths/trees may have a nonzero penalty if one is a prefix of the other.
Then we must compare the counter produced by the last shared node in order to determine
if the longer path does not reflect a later update than the prefix path, violating causality.
"""
def penalty(context, {:c, _}), do: penalty(context, :c)
def penalty([_ch | []], :c), do: 0
def penalty([ch | ct], :c) do
pch = ct
|> Enum.map(fn ctc -> cpenalty(ch, ctc) end)
|> Enum.max
max(pch, penalty(ct, :c))
end
defp cpenalty([], []), do: 0
defp cpenalty([{s, n1} | []], [{s, n2}, _ | _]), do: n2-n1
defp cpenalty([{s, n1}, _ | _], [{s, n2} | []]), do: n1-n2
defp cpenalty([{s, _n1} | c1t], [{s, _n2} | c2t]), do: cpenalty(c1t, c2t)
defp cpenalty([{_s1, _n1} | _c1t], [{_s2, _n2} | _c2t]), do: 0
defp cpenalty([c1h | _], [{_s, _n} | _] = c2) when is_list(c1h) do
c1h
|> Enum.map(fn ctc -> cpenalty(c2, ctc) end)
|> Enum.max
end
defp cpenalty([{_s, _n} | _] = c1, [c2h | _]) when is_list(c2h) do
c2h
|> Enum.map(fn ctc -> cpenalty(c1, ctc) end)
|> Enum.max
end
defp cpenalty([c1h | c1t], [c2h | c2t]) when is_list(c1h) and is_list(c2h) do
cpenalty(c1t, c2t)
end
######################################
# TRANSFORMING INTERMEDIATE CONTEXTS #
###### (INTO FINALIZED CONTEXTS) #####
######################################
@doc """
Transforms a list of intermediate contexts into finalized (i.e. plain) contexts
according to their respective guarantees and by means of the given transformation data
"""
def transform([], [], []), do: []
def transform([c | ct], [t | tt], [g | gt]) do
[transform(c, t, g) | transform(ct, tt, gt)]
end
@doc """
Transforms a fifo context (update semantics) by leaving it 'as is'.
"""
def transform(c, _trans, {:fu, _m}), do: c
@doc """
Transforms a fifo context (propagate semantics) by leaving it 'as is'.
"""
def transform(c, _trans, {:fp, _m}), do: c
@doc """
Transforms a time-synchronization context by leaving it 'as is'.
"""
def transform(c, _trans, {:t, _m}), do: c
@doc """
Transforms a glitch-freedom context by leaving it 'as is'.
"""
def transform(c, _trans, {:g, _m}), do: c
@doc """
Transforms a causality context.
The transformation data is of the form {node, counter}
If the context is a path of nodes [{node, counter}],
just append the transformation data, completing the path.
If the context is a list of paths
"""
def transform(c, trans, {:c, _m}) do
case c do
[[_ | _]=path] -> path ++ trans
[[_ | _] | _] -> [c] ++ trans
end
end
################################
# CREATING CONTEXT OBSERVABLES #
################################
@doc """
Creates an observable carrying the contexts
for the respective values of a given observable under fifo with update semantics.
"""
def new_context_obs(obs, {:fu, _m}) do
Obs.count(obs, 0)
|> Obs.map(fn _ -> nil end)
end
@doc """
Creates an observable carrying the contexts
for the respective values of a given observable under fifo with propagate semantics.
"""
def new_context_obs(obs, {:fp, _m}) do
Obs.count(obs, 0)
|> Obs.map(fn _ -> nil end)
end
@doc """
Creates an observable carrying the contexts
for the respective values of a given observable under glitch-freedom.
"""
def new_context_obs(obs, {:g, _m}) do
{_f, pid} = obs
Obs.count(obs, 0)
|> Obs.map(fn n -> [{{node(), pid}, n-1}] end)
end
@doc """
Creates an observable carrying the contexts
for the respective values of a given observable under time-synchronization.
"""
def new_context_obs(obs, {:t, _m}) do
Obs.count(obs, 0)
|> Obs.map(fn n -> n-1 end)
end
@doc """
Creates an observable carrying the contexts
for the respective values of a given observable under causality.
"""
def new_context_obs(obs, {:c, _m}) do
{_f, pid} = obs
Obs.count(obs, 0)
|> Obs.map(fn n -> [{{node(), pid}, n-1}] end)
end
end