2016-02-05 12 views
6

Ho un'applicazione web in elisir che assomiglia a questoCaching calcolo costoso in elisir

defmodule Test do 
    use Plug.Router 

    plug :match 
    plug :dispatch 

    def expensiveComputation() do 
    // performs an expensive computation and 
    // returns a list 
    end 

    get "/randomElement" do 
    randomElement = expensiveComputation() |> Enum.random 
    send_resp(conn, 200, randomElement) 
    end 

end 

Ogni volta che inviare una richiesta GET-/randomElement, expensiveComputation viene chiamato. La funzione expensiveComputation richiede molto tempo per l'esecuzione ma restituisce la stessa cosa ogni volta che viene chiamato. Qual è il modo più semplice per memorizzare nella cache il risultato in modo che venga eseguito una sola volta all'avvio?

risposta

5

In Elixir quando vuoi stato in cui quasi sempre bisogno di avere un processo per mantenere quello stato. Il modulo Agent è particolarmente adatto per il tipo di operazione che si desidera - semplicemente avvolgendo un certo valore e accedervi. Qualcosa del genere dovrebbe funzionare:

defmodule Cache do 
    def start_link do 
    initial_state = expensive_computation 
    Agent.start_link(fn -> initial_state end, name: __MODULE__) 
    end 

    def get(f \\ &(&1)) do 
    Agent.get(__MODULE__, f) 
    end 

    defp expensive_computation do 
    # ... 
    end 
end 

Quindi è possibile collegare Cache nel vostro albero di supervisione normalmente, e proprio Cache.get quando è necessario il risultato di expensive_computation.

Si prega di notare che questo sarà letteralmente eseguito expensive_computation all'avvio - nel processo di portare il vostro albero di supervisione. Se è molto costoso - dell'ordine di 10 secondi o più - si potrebbe desiderare di spostare il calcolo al processo Agent:

def start_link do 
    Agent.start_link(fn -> expensive_computation end, name: __MODULE__) 
end 

È necessario gestire il caso della cache essere vuota in quel caso , mentre nel primo esempio l'avvio è bloccato fino expensive_computation è finito. Si può usare mettendo i lavoratori in funzione della Cache più tardi l'ordine di avvio.

6

È possibile utilizzare ETS per memorizzare calcoli costosi. Ecco qualcosa che ho scritto di recente, potrebbe non essere una soluzione a tutti gli effetti di caching, ma funziona bene per me:

defmodule Cache do 
    @table __MODULE__ 

    def start do 
    :ets.new @table, [:named_table, read_concurrency: true] 
    end 

    def fetch(key, expires_in_seconds, fun) do 
    case lookup(key) do 
     {:hit, value} -> 
     value 
     :miss -> 
     value = fun.() 
     put(key, expires_in_seconds, value) 
     value 
    end 
    end 

    defp lookup(key) do 
    case :ets.lookup(@table, key) do 
     [{^key, expires_at, value}] -> 
     case now < expires_at do 
      true -> {:hit, value} 
      false -> :miss 
     end 
     _ -> 
     :miss 
    end 
    end 

    defp put(key, expires_in_seconds, value) do 
    expires_at = now + expires_in_seconds 
    :ets.insert(@table, {key, expires_at, value}) 
    end 

    defp now do 
    :erlang.system_time(:seconds) 
    end 
end 

Prima di tutto bisogna chiamare Cache.start da qualche parte, in modo verrà creato il tavolo ETS (per esempio in funzione start della tua app). Quindi è possibile utilizzare in questo modo:

value = Cache.fetch cache_key, expires_in_seconds, fn -> 
    # expensive computation 
end 

Ad esempio:

Enum.each 1..100000, fn _ -> 
    message = Cache.fetch :slow_hello_world, 1, fn -> 
    :timer.sleep(1000) # expensive computation 
    "Hello, world at #{inspect :calendar.local_time}!" 
    end 
    IO.puts message 
end