Managing State with Processes
Functional programs are stateless,改用 Process 保存 state
新增 /rumbl/ib/rumbl/counter.ex
defmodule Rumbl.Counter do
def inc(pid), do: send(pid, :inc)
def dec(pid), do: send(pid, :dec)
def val(pid, timeout \\ 5000) do
ref = make_ref()
send(pid, {:val, self(), ref})
receive do
{^ref, val} -> val
after timeout -> exit(:timeout)
end
end
def start_link(initial_val) do
{:ok, spawn_link(fn -> listen(initial_val) end)}
end
defp listen(val) do
receive do
:inc -> listen(val + 1)
:dec -> listen(val - 1)
{:val, sender, ref} ->
send sender, {ref, val}
listen(val)
end
end
end
這是一個獨立的 counter service,Counter API 有三個
- :inc
- :dec
- :val
:inc :dec 是非同步的呼叫
:val 不同,發送 message 後,會用 receive 等待回應。
make_ref() 是一個 global unique reference,可在 global (cluster) 環境中運作。
^ref 表示我們是以 pattern matching 的方式,判斷是不是回傳了正確的 process reference
OTP 需要一個 startlink function,並以 initialval 為 counter 初始的 state
程式中沒有看到任何 global variable 儲存 state,而是呼叫 listen,listen 會以 receive 去 block 並等待 message,而 val 就是放在這個 function 的參數上。process state 是用 recursive function 的方式不斷重複發送給下一個 listen,這是 tail recursive。
測試 Counter
$ iex -S mix
iex(1)> alias Rumbl.Counter
Rumbl.Counter
iex(2)> {:ok, counter} = Counter.start_link(0)
{:ok, #PID<0.270.0>}
iex(3)> Counter.inc(counter)
:inc
iex(4)> Counter.inc(counter)
:inc
iex(5)> Counter.val(counter)
2
iex(6)> Counter.dec(counter)
:dec
iex(7)> Counter.val(counter)
1
Building GenServer for OTP
更新 /lib/rumbl/counter.ex
defmodule Rumbl.Counter do
use GenServer
def inc(pid), do: GenServer.cast(pid, :inc)
def dec(pid), do: GenServer.cast(pid, :dec)
def val(pid) do
GenServer.call(pid, :val)
end
def start_link(initial_val) do
GenServer.start_link(__MODULE__, initial_val)
end
def init(initial_val) do
{:ok, initial_val}
end
def handle_cast(:inc, val) do
{:noreply, val + 1}
end
def handle_cast(:dec, val) do
{:noreply, val - 1}
end
def handle_call(:val, _from, val) do
{:reply, val, val}
end
end
GenServer.cast 是非同步呼叫,server 以 handle_cast 處理,最後會 return {:noreply, val + 1} ,因為呼叫者不需要這個 reply message
GenServer.call 是同步呼叫,server 以 handle_call 處理
測試
$ iex -S mix
iex(1)> alias Rumbl.Counter
Rumbl.Counter
iex(2)> {:ok, counter} = Counter.start_link(0)
{:ok, #PID<0.269.0>}
iex(3)> Counter.inc(counter)
:ok
iex(4)> Counter.val(counter)
1
iex(5)> Counter.dec(counter)
:ok
iex(6)> Counter.val(counter)
0
Adding Failover
利用 Supervisor 監控 counter
Phoenix 並沒有很多處理 fail exception 的 code,而是以 error reporting 的方式處理,同時加上自動 restart service。
修改 /lib/rumbl.ex
defmodule Rumbl do
use Application
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
supervisor(Rumbl.Endpoint, []),
supervisor(Rumbl.Repo, []),
worker(Rumbl.Counter, [5]), # new counter worker
]
opts = [strategy: :one_for_one, name: Rumbl.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
Rumbl.Endpoint.config_change(changed, removed)
:ok
end
end
child spec 定義 Elixir application 要啟動的 children,將 counter 這個 worker 加入 children list。
opts 是 supervision policy,這裡是使用 :oneforone
:oneforall 是 kill and restart all child processes
在 /lib/rumbl/counter.ex 加上 :tick 及處理 :tick 的 handle_info,每 1000ms 就倒數一次
def init(initial_val) do
Process.send_after(self, :tick, 1000)
{:ok, initial_val}
end
def handle_info(:tick, val) do
IO.puts "tick #{val}"
Process.send_after(self, :tick, 1000)
{:noreply, val - 1}
end
再加上一點檢查,只倒數到 0 ,就會 raise exception,OTP process 會 crash
def init(initial_val) do
Process.send_after(self, :tick, 1000)
{:ok, initial_val}
end
def handle_info(:tick, val) when val <= 0, do: raise "boom!"
def handle_info(:tick, val) do
IO.puts "tick #{val}"
Process.send_after(self, :tick, 1000)
{:noreply, val - 1}
end
但可發現它會自動重新啟動
$ iex -S mix
iex(1)> tick 5
tick 4
tick 3
tick 2
tick 1
[error] GenServer #PID<0.365.0> terminating
** (RuntimeError) boom!
(rumbl) lib/rumbl/counter.ex:21: Rumbl.Counter.handle_info/2
(stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:686: :gen_server.handle_msg/6
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: :tick
State: 0
tick 5
tick 4
tick 3
tick 2
tick 1
Restart Strategies
預設 child processes 的 restart strategy 是 :permanent,也可以用 restart 指定
worker(Rumbl.Counter, [5], restart: :permanent),
:permanent
child is always restarted (default)
:temporary
child is never restarted
:transient
只在異常終止時 restart,也就是 :normal, :shutdown, {:shutdown, term} 以外的 exit reason
另外還有 maxrestarts 及 maxseconds 參數,在 maxsecodns 時間內可以 restart maxrestarts 次
預設是 3 restarts in 5 seconds
Supervision Strategies
:oneforone
a child terminates -> supervisor restars only that process
:oneforall
a child tetminates -> supervisor terminates all children and restarts them
:restforone
a child terminates -> supervisor terminates all child processes defiend after the one that dies,並 restart all terminated processes
:simpleonefor_one
類似 :oneforone,用在 supervisor 需要動態 supervise processes 的情況,例如 web server 需要 supervise web requests,通常有 10 ~ 100,000 個 concurrent running processes
把 strategy 換成 :oneforall 測試
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
supervisor(Rumbl.Endpoint, []),
supervisor(Rumbl.Repo, []),
worker(Rumbl.Counter, [5]), # new counter worker
]
opts = [strategy: :one_for_all, name: Rumbl.Supervisor]
Supervisor.start_link(children, opts)
end
啟動 Phoenix server 會發現 Cowboy 也 restart
tick 2
tick 1
[error] GenServer #PID<0.348.0> terminating
** (RuntimeError) boom!
(rumbl) lib/rumbl/counter.ex:21: Rumbl.Counter.handle_info/2
(stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:686: :gen_server.handle_msg/6
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: :tick
State: 0
[info] Running Rumbl.Endpoint with Cowboy using http://localhost:4000
tick 5
現在把 worker 拿掉,strategy 改回 :oneforone
children = [
supervisor(Rumbl.Endpoint, []),
supervisor(Rumbl.Repo, [])
]
opts = [strategy: :one_for_one, name: Rumbl.Supervisor]
Using Agents
agent 類似 GenServer,但只有 5 個 main functions
start_link 啟動 agent, stop 停止, update 更新 agent 狀態
$ iex -S mix
iex(1)> import Agent
Agent
iex(2)> {:ok, agent} = start_link fn -> 5 end
{:ok, #PID<0.258.0>}
iex(3)> update agent, &(&1 + 1)
:ok
iex(4)> get agent, &(&1)
6
iex(5)> stop agent
:ok
加上 :name option
iex(7)> {:ok, agent} = start_link fn -> 5 end, name: MyAgent
{:ok, #PID<0.265.0>}
iex(8)> update MyAgent, &(&1 + 1)
:ok
iex(9)> get MyAgent, &(&1)
6
iex(10)> stop MyAgent
:ok
重複名稱會發生 error
iex(11)> {:ok, agent} = start_link fn -> 5 end, name: MyAgent
{:ok, #PID<0.271.0>}
iex(12)> {:ok, agent} = start_link fn -> 5 end, name: MyAgent
** (MatchError) no match of right hand side value: {:error, {:already_started, #PID<0.271.0>}}
Phoenix.Channel 就是用 Agent 實作
Design an Information System with OTP
新增 /lib/rumbl/infosyssupervisor.ex
defmodule Rumbl.InfoSys.Supervisor do
use Supervisor
def start_link() do
Supervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_opts) do
children = [
worker(Rumbl.InfoSys, [], restart: :temporary)
]
supervise children, strategy: :simple_one_for_one
end
end
修改 /lib/rumbl.ex
children = [
supervisor(Rumbl.Endpoint, []),
supervisor(Rumbl.InfoSys.Supervisor, []), # new supervisor
supervisor(Rumbl.Repo, []),
# worker(Rumbl.Counter, [5]), # new counter worker
]
Building a start_link Proxy
啟動時動態決定要幾個 service,要製作一個 worker,可啟動多個 backends
proxy function 是 client, server 之間的 lightweight function interface
新增 /lib/rumbl/info_sys.ex
defmodule Rumbl.InfoSys do
@backends [Rumbl.InfoSys.Wolfram]
defmodule Result do
defstruct score: 0, text: nil, url: nil, backend: nil
end
def start_link(backend, query, query_ref, owner, limit) do
backend.start_link(query, query_ref, owner, limit)
end
def compute(query, opts \\ []) do
limit = opts[:limit] || 10
backends = opts[:backends] || @backends
backends
|> Enum.map(&spawn_query(&1, query, limit))
end
defp spawn_query(backend, query, limit) do
query_ref = make_ref()
opts = [backend, query, query_ref, self(), limit]
{:ok, pid} = Supervisor.start_child(Rumbl.InfoSys.Supervisor, opts)
{pid, query_ref}
end
end
InfoSys 跟一般 GenServer 有一點不同,裡面存放到 results 到 module attribute -> 所有支援的 backends 為 list
Result struct 會儲存每個 search result 的結果,還有 score 及 relevnace, text to describe the result, url
startlink 就是 proxy,會再呼叫其他 backend 的 startlink
compute 會 maps over all backends,呼叫每個 backend 的 spawn_query
Building the Wolfram into System
修改 /rumbl/mix.exs
{:sweet_xml, "~> 0.5.0"},
到 wolfram 申請帳號,及 APP ID
APP NAME: wolfram
APPID: LP93J3-XXXXXXXXXX
新增設定 /config/dev.secret.exs
use Mix.Config
config :rumbl, :wolfram, app_id: "LP93J3-XXXXXXXXXX"
記得將 /config/dev.secret.exs 放到 .gitignore
修改 /config/dev.exs,最後面加上
import_config "dev.secret.exs"
新增 /lib/rumbl/info_sys/wolfram.ex
defmodule Rumbl.InfoSys.Wolfram do
import SweetXml
alias Rumbl.InfoSys.Result
def start_link(query, query_ref, owner, limit) do
Task.start_link(__MODULE__, :fetch, [query, query_ref, owner, limit])
end
def fetch(query_str, query_ref, owner, _limit) do
query_str
|> fetch_xml()
|> xpath(~x"/queryresult/pod[contains(@title, 'Result') or
contains(@title, 'Definitions')]
/subpod/plaintext/text()")
|> send_results(query_ref, owner)
end
defp send_results(nil, query_ref, owner) do
send(owner, {:results, query_ref, []})
end
defp send_results(answer, query_ref, owner) do
results = [%Result{backend: "wolfram", score: 95, text: to_string(answer)}]
send(owner, {:results, query_ref, results})
end
defp fetch_xml(query_str) do
{:ok, {_, _, body}} = :httpc.request(
String.to_char_list("http://api.wolframalpha.com/v2/query" <>
"?appid=#{app_id()}" <>
"&input=#{URI.encode(query_str)}&format=plaintext"))
body
end
defp app_id, do: Application.get_env(:rumbl, :wolfram)[:app_id]
end
這個 module 並沒有 GenServer 的 callbacks,因為這個 process 是一個 task,GenServer 是一個 generic server 可計算並儲存 state,但有時我們只需要 store state 或是 只需要執行某個 function。
Agent 是簡化的 GenServer 可儲存 state
task 是個簡單的 process 可執行某個 function
SweetXml 用來 parse XML,Result 是 the struct for the results
Task.start_link 是啟動 Task 的方式
fetch_xml 裡面試用 :httpc,這是 erlang 的 standard library,可處理 HTTP request
sendresults(queryref, owner) 將結果回傳給 requester
有兩種 send_results,分為有 answer 或沒有
先用 iex -S mix 測試
iex(4)> Rumbl.InfoSys.compute("what is elixir?")
[{#PID<0.592.0>, #Reference<0.3775272462.1982070785.26789>}]
iex(5)> flush()
:ok
iex(6)> flush()
:ok
iex(7)> flush()
{:results, #Reference<0.3775272462.1982070785.26789>,
[%Rumbl.InfoSys.Result{backend: "wolfram", score: 95,
text: "1 | noun | a sweet flavored liquid (usually containing a small amount of alcohol) used in compounding medicines to be taken by mouth in order to mask an unpleasant taste\n2 | noun | hypothetical substance that the alchemists believed to be capable of changing base metals into gold\n3 | noun | a substance believed to cure all ills",
url: nil}]}
:ok
要讓 service 更堅固,必須做以下工作
偵測 backend crash,這樣就不要等 results
由 backend 取得結果要根據 score 排序
需要 timeout 機制
Monitoring Processes
使用 Process.monitor 在 waiting results 時偵測 backend crashes,一但設定了 monitor,會在該 process dies 時,收到 message。
測試
iex(1)> pid = spawn(fn -> :ok end)
#PID<0.261.0>
iex(2)> Process.monitor(pid)
#Reference<0.777943872.2254438401.3282>
iex(3)> flush()
{:DOWN, #Reference<0.777943872.2254438401.3282>, :process, #PID<0.261.0>,
:noproc}
:ok
修改 /lib/rumbl/info_sys.ex
defmodule Rumbl.InfoSys do
@backends [Rumbl.InfoSys.Wolfram]
defmodule Result do
defstruct score: 0, text: nil, url: nil, backend: nil
end
def start_link(backend, query, query_ref, owner, limit) do
backend.start_link(query, query_ref, owner, limit)
end
def compute(query, opts \\ []) do
limit = opts[:limit] || 10
backends = opts[:backends] || @backends
backends
|> Enum.map(&spawn_query(&1, query, limit))
|> await_results(opts)
|> Enum.sort(&(&1.score >= &2.score))
|> Enum.take(limit)
end
defp spawn_query(backend, query, limit) do
query_ref = make_ref()
opts = [backend, query, query_ref, self(), limit]
{:ok, pid} = Supervisor.start_child(Rumbl.InfoSys.Supervisor, opts)
monitor_ref = Process.monitor(pid)
{pid, monitor_ref, query_ref}
end
defp await_results(children, _opts) do
await_result(children, [], :infinity)
end
defp await_result([head|tail], acc, timeout) do
{pid, monitor_ref, query_ref} = head
receive do
{:results, ^query_ref, results} ->
Process.demonitor(monitor_ref, [:flush])
await_result(tail, results ++ acc, timeout)
{:DOWN, ^monitor_ref, :process, ^pid, _reason} ->
await_result(tail, acc, timeout)
end
end
defp await_result([], acc, _) do
acc
end
end
compute 會自動等待 results,收到時,會 sorting by score,並回報 top ones
spawn_query 裡面增加了 Process.monitor(pid)
awaitresults 是 recursive function,在每次呼叫 awaitresults 就會新增一個 result 到 list
正確的 result 會 match {:results, ^query_ref, result}
Process.demonitor(monitor_ref, [:flush]) 是將 monitor process 移除
現在 compute 會自動處理結果
iex(1)> Rumbl.InfoSys.compute("what is the meaning of life?")
[%Rumbl.InfoSys.Result{backend: "wolfram", score: 95,
text: "42\n(according to the book The Hitchhiker's Guide to the Galaxy, by Douglas Adams)",
url: nil}]
Timeout
receive 可設定 after 這個 timeout 機制
receive do
:this_will_never_arrive -> :ok
after
1_000 -> :timedout
end
修改 /lib/rumbl/infosys.ex 等待 backend 5000 ms
defmodule Rumbl.InfoSys do
@backends [Rumbl.InfoSys.Wolfram]
defmodule Result do
defstruct score: 0, text: nil, url: nil, backend: nil
end
def start_link(backend, query, query_ref, owner, limit) do
backend.start_link(query, query_ref, owner, limit)
end
def compute(query, opts \\ []) do
limit = opts[:limit] || 10
backends = opts[:backends] || @backends
backends
|> Enum.map(&spawn_query(&1, query, limit))
|> await_results(opts)
|> Enum.sort(&(&1.score >= &2.score))
|> Enum.take(limit)
end
defp spawn_query(backend, query, limit) do
query_ref = make_ref()
opts = [backend, query, query_ref, self(), limit]
{:ok, pid} = Supervisor.start_child(Rumbl.InfoSys.Supervisor, opts)
monitor_ref = Process.monitor(pid)
{pid, monitor_ref, query_ref}
end
defp await_results(children, opts) do
timeout = opts[:timeout] || 5000
timer = Process.send_after(self(), :timedout, timeout)
results = await_result(children, [], :infinity)
cleanup(timer)
results
end
defp await_result([head|tail], acc, timeout) do
{pid, monitor_ref, query_ref} = head
receive do
{:results, ^query_ref, results} ->
Process.demonitor(monitor_ref, [:flush])
await_result(tail, results ++ acc, timeout)
{:DOWN, ^monitor_ref, :process, ^pid, _reason} ->
await_result(tail, acc, timeout)
:timedout ->
kill(pid, monitor_ref)
await_result(tail, acc, 0)
after
timeout ->
kill(pid, monitor_ref)
await_result(tail, acc, 0)
end
end
defp await_result([], acc, _) do
acc
end
defp kill(pid, ref) do
Process.demonitor(ref, [:flush])
Process.exit(pid, :kill)
end
defp cleanup(timer) do
:erlang.cancel_timer(timer)
receive do
:timedout -> :ok
after
0 -> :ok
end
end
end
Integrating OTP Services with Channels
將剛剛的服務放到 VideoChannel 中
修改 /web/channels/video_channel.ex
defmodule Rumbl.VideoChannel do
use Rumbl.Web, :channel
alias Rumbl.AnnotationView
def join("videos:" <> video_id, params, socket) do
last_seen_id = params["last_seen_id"] || 0
video_id = String.to_integer(video_id)
video = Repo.get!(Rumbl.Video, video_id)
annotations = Repo.all(
from a in assoc(video, :annotations),
where: a.id > ^last_seen_id,
order_by: [asc: a.at, asc: a.id],
limit: 200,
preload: [:user]
)
resp = %{annotations: Phoenix.View.render_many(annotations, AnnotationView,
"annotation.json")}
{:ok, resp, assign(socket, :video_id, video.id)}
end
def handle_in(event, params, socket) do
user = Repo.get(Rumbl.User, socket.assigns.user_id)
handle_in(event, params, user, socket)
end
def handle_in("new_annotation", params, user, socket) do
changeset =
user
|> build_assoc(:annotations, video_id: socket.assigns.video_id)
|> Rumbl.Annotation.changeset(params)
case Repo.insert(changeset) do
{:ok, ann} ->
broadcast_annotation(socket, ann)
Task.start_link(fn -> compute_additional_info(ann, socket) end)
{:reply, :ok, socket}
{:error, changeset} ->
{:reply, {:error, %{errors: changeset}}, socket}
end
end
defp broadcast_annotation(socket, annotation) do
annotation = Repo.preload(annotation, :user)
rendered_ann = Phoenix.View.render(AnnotationView, "annotation.json", %{
annotation: annotation
})
broadcast! socket, "new_annotation", rendered_ann
end
defp compute_additional_info(ann, socket) do
for result <- Rumbl.InfoSys.compute(ann.body, limit: 1, timeout: 10_000) do
attrs = %{url: result.url, body: result.text, at: ann.at}
info_changeset =
Repo.get_by!(Rumbl.User, username: result.backend)
|> build_assoc(:annotations, video_id: ann.video_id)
|> Rumbl.Annotation.changeset(attrs)
case Repo.insert(info_changeset) do
{:ok, info_ann} -> broadcast_annotation(socket, info_ann)
{:error, _changeset} -> :ignore
end
end
end
end
新增 /rumbl/priv/repo/backend_seeds.exs
alias Rumbl.Repo
alias Rumbl.User
Repo.insert!(%User{name: "Wolfram", username: "wolfram"})
$ mix run priv/repo/backend_seeds.exs
Compiling 19 files (.ex)
warning: String.to_char_list/1 is deprecated, use String.to_charlist/1
lib/rumbl/info_sys/wolfram.ex:29
warning: function authenticate/2 is unused
web/controllers/user_controller.ex:40
[debug] QUERY OK db=0.2ms queue=12.1ms
begin []
[debug] QUERY OK db=0.9ms
INSERT INTO `users` (`name`,`username`,`inserted_at`,`updated_at`) VALUES (?,?,?,?) ["Wolfram", "wolfram", {{2017, 9, 7}, {3, 55, 52, 165910}}, {{2017, 9, 7}, {3, 55, 52, 168676}}]
[debug] QUERY OK db=1.8ms
commit []
Inpsecting with Observer
erlang 有個 Observer 工具,可用這個方式啟動
iex -S mix
# 用這個方式啟動,可查看 Phoenix 部分的狀況
iex -S mix phoenix.server
supervision tree 是一個好的工具,可以觀察要怎麼將 application 分開。現在我們要將 application 分成兩個部分 :rumbl 及 :info_sys
利用 umbrella project 來處理
Using Umbrellas
每個 umbrella project 目錄包含以下的部分
- shared configuration of the project
- dependencies for the project
- apps 目錄 with child applications
要重新產生一個 umbrella project
$ mix new rumbrella --umbrella
* creating .gitignore
* creating README.md
* creating mix.exs
* creating apps
* creating config
* creating config/config.exs
Your umbrella project was created successfully.
Inside your project, you will find an apps/ directory
where you can create and host many apps:
cd rumbrella
cd apps
mix new my_app
Commands like "mix compile" and "mix test" when executed
in the umbrella project root will automatically run
for each application in the apps/ directory.
將 InfoSys 移動到 rumbrella 下面
$ cd rumbrella/apps
$ mix new info_sys --sup
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/info_sys.ex
* creating lib/info_sys/application.ex
* creating test
* creating test/test_helper.exs
* creating test/info_sys_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd info_sys
mix test
Run "mix help" for more commands.
將 InfoSys 由 rumbl 移到 info_sys
將 rumbl/lib/rumbl/infosys.ex Rumbl.InfoSys 改為 InfoSys 並複製到 infosys/lib/info_sys.ex ,前面加上
use Application
def start(_type, _args) do
InfoSys.Supervisor.start_link()
end
@backends [InfoSys.Wolfram]
將 /rumbl/lib/rumbl/infosys/supervisor.ex Rumbl.InfoSys.Supervisor 改為 InfoSys.Supervisor 並複製到 infosys/lib/info_sys/supervisor.ex
將 /rumbl/lib/rumbl/infosys/wolfram.ex 的 Rumbl.InfoSys.Wolfram module 改為 InfoSys.Wolfram,並複製到 infosys/lib/info_sys/wolfram.ex
把所有 "Rumbl.InfoSys" 都改為 "InfoSys"
修改 lib/infosys/wolfram.ex,由 :rumbl 改為 :infosys
defp app_id, do: Application.get_env(:info_sys, :wolfram)[:app_id]
修改 apps/info_sys/mix.exs
def deps do
[
{:sweet_xml, "~> 0.5.0"}
]
end
在 rumbrella 目錄執行 $ mix deps.get
將 rumbl 改為 umbrella child
將整個 rumbl 移動到 apps 目錄
修改 rumbrella/apps/rumbl/mix.exs,增加
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
修改 applicaton ,增加 :info_sys
def application do
[mod: {Rumbl, []},
applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
:phoenix_ecto, :mariaex, :comeonin, :info_sys]]
end
更新 dependencies,移除 :sweetxml ,增加 :infosys
{:info_sys, in_umbrella: true},
修改 rumbl/lib/rumbl.ex 移除 Rumbl.InfoSys
children = [
supervisor(Rumbl.Endpoint, []),
supervisor(Rumbl.Repo, []),
]
修改 /web/channlel/videochannel.ex 的 computeadditional_info 將 Rumbl.InfoSys 改為 InfoSys
修改 cofnig/dev.secrets.exs 改為 :info_sys
use Mix.Config
config :info_sys, :wolfram, app_id: "LP93J3-XXXXXX"
修改 rumbl/package.json
"dependencies": {
"phoenix": "file:../../deps/phoenix",
"phoenix_html": "file:../../deps/phoenix_html"
},
在 rumbrella 目錄中
$ mix deps.get
$ mix test
References
Programming Phoenix