You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2019/05/16 23:18:21 UTC

[couchdb] branch prototype/rfc-couch-jobs created (now f8c2411)

This is an automated email from the ASF dual-hosted git repository.

vatamane pushed a change to branch prototype/rfc-couch-jobs
in repository https://gitbox.apache.org/repos/asf/couchdb.git.


      at f8c2411  CouchDB background jobs WIP

This branch includes the following new commits:

     new f8c2411  CouchDB background jobs WIP

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[couchdb] 01/01: CouchDB background jobs WIP

Posted by va...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

vatamane pushed a commit to branch prototype/rfc-couch-jobs
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit f8c2411739082c4708f4b87b97d314a070d6f060
Author: Nick Vatamaniuc <va...@apache.org>
AuthorDate: Thu May 16 19:11:50 2019 -0400

    CouchDB background jobs WIP
    
    So far got:
     - Main API module (couch_jobs.er)
     - Supervisor and app structures
     - FDB read/write code in couch_jobs_fdb.erl
    
    In couch_jobs_fdb.erl have:
    
     - All jobs creation API: add(), remove(), submit(), get_job()
     - Half of the worker API: finish(), resubmit()
     - Directory path caching
     - Metadata changes check
    
    Still need:
     - Finish worker API: accept() and update()
     - Activity monitor implemenation
     - An example worker of some sort (Maybe activity monitor can be worker as well...)
---
 rebar.config.script                   |   1 +
 rel/reltool.config                    |   2 +
 src/couch_jobs/.gitignore             |   4 +
 src/couch_jobs/README.md              |   5 +
 src/couch_jobs/src/couch_jobs.app.src |  30 +++
 src/couch_jobs/src/couch_jobs.erl     |  96 +++++++++
 src/couch_jobs/src/couch_jobs_app.erl |  26 +++
 src/couch_jobs/src/couch_jobs_fdb.erl | 358 ++++++++++++++++++++++++++++++++++
 src/couch_jobs/src/couch_jobs_sup.erl |  46 +++++
 9 files changed, 568 insertions(+)

diff --git a/rebar.config.script b/rebar.config.script
index 3b58bcb..2def724 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -76,6 +76,7 @@ SubDirs = [
     "src/couch_tests",
     "src/ddoc_cache",
     "src/fabric",
+    "src/couch_jobs",
     "src/global_changes",
     "src/mango",
     "src/rexi",
diff --git a/rel/reltool.config b/rel/reltool.config
index 1051d2e..afebc44 100644
--- a/rel/reltool.config
+++ b/rel/reltool.config
@@ -34,6 +34,7 @@
         couch,
         couch_epi,
         couch_index,
+        couch_jobs,
         couch_log,
         couch_mrview,
         couch_plugins,
@@ -90,6 +91,7 @@
     {app, config, [{incl_cond, include}]},
     {app, couch, [{incl_cond, include}]},
     {app, couch_epi, [{incl_cond, include}]},
+    {app, couch_jobs, [{incl_cond, include}]},
     {app, couch_index, [{incl_cond, include}]},
     {app, couch_log, [{incl_cond, include}]},
     {app, couch_mrview, [{incl_cond, include}]},
diff --git a/src/couch_jobs/.gitignore b/src/couch_jobs/.gitignore
new file mode 100644
index 0000000..62c8b07
--- /dev/null
+++ b/src/couch_jobs/.gitignore
@@ -0,0 +1,4 @@
+*.beam
+.eunit
+ebin/couch_workers.app
+.DS_Store
\ No newline at end of file
diff --git a/src/couch_jobs/README.md b/src/couch_jobs/README.md
new file mode 100644
index 0000000..b2910a5
--- /dev/null
+++ b/src/couch_jobs/README.md
@@ -0,0 +1,5 @@
+CouchDB Jobs Application
+=========================
+
+Run background jobs in CouchDB
+
diff --git a/src/couch_jobs/src/couch_jobs.app.src b/src/couch_jobs/src/couch_jobs.app.src
new file mode 100644
index 0000000..51de1ab
--- /dev/null
+++ b/src/couch_jobs/src/couch_jobs.app.src
@@ -0,0 +1,30 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+{application, couch_jobs, [
+    {description, "CouchDB Jobs"},
+    {vsn, git},
+    {mod, {couch_jobs_app, []}},
+    {registered, [
+        couch_jobs_sup,
+        couch_jobs_global,
+        couch_jobs_local
+    ]},
+    {applications, [
+        kernel,
+        stdlib,
+        erlfdb,
+        couch_log,
+        config,
+        fabric
+    ]}
+]}.
diff --git a/src/couch_jobs/src/couch_jobs.erl b/src/couch_jobs/src/couch_jobs.erl
new file mode 100644
index 0000000..6166706
--- /dev/null
+++ b/src/couch_jobs/src/couch_jobs.erl
@@ -0,0 +1,96 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_jobs).
+
+-export([
+    add/3,
+    remove/2,
+    resubmit/2,
+    get_job/2,
+
+    accept/1,
+    accept/2,
+    finish/5,
+    resubmit/4,
+    update/5
+]).
+
+
+%% Job Creation API
+
+add(Type, JobId, JobOpts) ->
+    try
+        ok = validate_jobopts(JobOpts)
+    catch
+        Tag:Err -> {error, {invalid_job_args, Tag, Err}}
+    end,
+    couch_jobs_fdb:tx(couch_jobs_fdb:get_jtx(), fun(JTx) ->
+        couch_job_fdb:add(JTx, Type, JobId, JobOpts)
+    end).
+
+
+remove(Type, JobId) ->
+    % Add the bit about cancelling the job if it is running
+    % and waiting for it to stop, then remove it.
+    couch_jobs_fdb:tx(couch_jobs_fdb:get_jtx(), fun(JTx) ->
+        couch_jobs_fdb:remove(JTx, Type, JobId)
+    end).
+
+
+resubmit(Type, JobId) ->
+    couch_jobs_fdb:tx(couch_jobs_fdb:get_jtx(), fun(JTx) ->
+        couch_jobs_fdb:resubmit(JTx, Type, JobId)
+    end).
+
+
+get_job(Type, JobId) ->
+    couch_jobs_fdb:tx(couch_jobs_fdb:get_jtx(), fun(JTx) ->
+        couch_jobs_fdb:get_job(JTx, Type, JobId)
+    end).
+
+
+%% Worker Implementation API
+
+accept(Type) ->
+    accept(Type, undefined).
+
+
+accept(Type, MaxPriority) ->
+    couch_jobs_fdb:tx(couch_jobs_fdb:get_jtx(), fun(JTx) ->
+        couch_jobs_fdb:accept(JTx, Type, MaxPriority)
+    end).
+
+
+finish(Tx, Type, JobId, JobOpts, WorkerLockId) ->
+    couch_jobs_fdb:tx(couch_jobs_fdb:get_jtx(Tx), fun(JTx) ->
+        couch_jobs_fdb:finish(JTx, Type, JobId, JobOpts, WorkerLockId)
+    end).
+
+
+resubmit(Tx, Type, JobId, WorkerLockId) ->
+    couch_jobs_fdb:tx(couch_jobs_fdb:get_jtx(Tx), fun(JTx) ->
+        couch_jobs_fdb:resubmit(JTx, Type, JobId, WorkerLockId)
+    end).
+
+
+update(Tx, Type, JobId, JobOpts, WorkerLockId) ->
+    couch_jobs_fdb:tx(couch_jobs_fdb:get_jtx(Tx), fun(JTx) ->
+        couch_jobs_fdb:update(JTx, Type, JobId, JobOpts, WorkerLockId)
+    end).
+
+
+%% Private utils
+
+validate_jobopts(#{} = JobOpts) ->
+    jiffy:encode(JobOpts),
+    ok.
diff --git a/src/couch_jobs/src/couch_jobs_app.erl b/src/couch_jobs/src/couch_jobs_app.erl
new file mode 100644
index 0000000..720b948
--- /dev/null
+++ b/src/couch_jobs/src/couch_jobs_app.erl
@@ -0,0 +1,26 @@
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_jobs_app).
+
+
+-behaviour(application).
+
+
+-export([
+    start/2,
+    stop/1
+]).
+
+
+start(_Type, []) ->
+    couch_jobs_sup:start_link().
+
+
+stop([]) ->
+    ok.
diff --git a/src/couch_jobs/src/couch_jobs_fdb.erl b/src/couch_jobs/src/couch_jobs_fdb.erl
new file mode 100644
index 0000000..2f58591
--- /dev/null
+++ b/src/couch_jobs/src/couch_jobs_fdb.erl
@@ -0,0 +1,358 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_jobs_fdb).
+
+-export([
+    add/4,
+    remove/3,
+    resubmit/3,
+    get_job/3,
+
+    accept/2,
+    accept/3,
+    finish/5,
+    resubmit/4,
+    update/5,
+
+    init_cache/0,
+
+    get_jtx/0,
+    get_jtx/1,
+
+    tx/1
+]).
+
+
+% JobOpts field definitions
+%
+-define(OPT_PRIORITY, <<"priority">>).
+-define(OPT_DATA, <<"data">>).
+-define(OPT_CANCEL, <<"cancel">>).
+-define(OPT_RESUBMIT, <<"resubmit">>).
+
+% These might be in a fabric public .hrl eventually
+%
+-define(uint2bin(I), binary:encode_unsigned(I, little)).
+-define(bin2uint(I), binary:decode_unsigned(I, little)).
+-define(UNSET_VS, {versionstamp, 16#FFFFFFFFFFFFFFFF, 16#FFFF}).
+-define(METADATA_VERSION_KEY, <<"$metadata_version_key$">>).
+
+% Data model definitions
+% Switch these to numbers eventually.
+%
+-define(JOBS, <<"couch_jobs">>).
+-define(DATA, <<"data">>).
+-define(PENDING, <<"pending">>).
+-define(WATCHES, <<"watches">>).
+-define(ACTIVITY_TIMEOUT, <<"activity_timeout">>).
+-define(ACTIVITY, <<"activity">>).
+
+%%% Data model %%%
+
+%% (?JOBS, ?DATA, Type, JobId) = (Sequence, WorkerLockId, Priority, JobOpts)
+%% (?JOBS, ?PENDING, Type, Priority, JobId) = ""
+%% (?JOBS, ?WATCHES, Type) = Sequence
+%% (?JOBS, ?ACTIVITY_TIMEOUT, Type) = ActivityTimeout
+%% (?JOBS, ?ACTIVITY, Type, Sequence) = JobId
+
+
+add(#{jtx := true} = JTx0, Type, JobId, JobOpts) ->
+    #{tx := Tx, jobs_path := JobsPath} = JTx = get_jtx(JTx0),
+    Key = pack({?DATA, Type, JobId}, JobsPath),
+    case erlfdb:wait(erlfdb:get(Tx, Key)) of
+        <<_/binary>> ->
+            {error, duplicate_job};
+        not_found ->
+            Priority = maps:get(?OPT_PRIORITY, JobOpts, ?UNSET_VS),
+            Val = pack({null, null, Priority, jiffy:encode(JobOpts)}),
+            case has_versionstamp(Priority) of
+                true -> erlfdb:set_versionstamped_value(Tx, Key, Val);
+                false -> erlfdb:set(Tx, Key, Val)
+            end,
+            pending_enqueue(JTx, Type, Priority, JobId),
+            ok
+    end.
+
+
+remove(#{jtx := true} = JTx0, Type, JobId) ->
+    #{tx := Tx, jobs_path :=  JobsPath} = JTx = get_jtx(JTx0),
+    Key = pack({?DATA, Type, JobId}, JobsPath),
+    case get_job(Tx, Key) of
+        {_, WorkerLockId, _, _} when WorkerLockId =/= null ->
+            {error, job_is_running};
+        {_, _, null, _,  _} ->
+            erlfdb:clear(Tx, Key);
+        {_, _, Priority, _, _} ->
+            pending_remove(JTx, Type, Priority, JobId),
+            erlfdb:clear(Tx, Key);
+        not_found ->
+            not_found
+    end.
+
+
+resubmit(#{jtx := true} = JTx, Type, JobId) ->
+    #{tx := Tx, jobs_path :=  JobsPath} = get_jtx(JTx),
+    Key = pack({?DATA, Type, JobId}, JobsPath),
+    case get_job(Tx, Key) of
+        {_, _, _, #{?OPT_RESUBMIT := true}} ->
+            ok;
+        {Seq, WorkerLockId, Priority, #{} = JobOpts} ->
+            set_resubmit(Tx, Key, Seq, WorkerLockId, Priority, JobOpts);
+        not_found ->
+            not_found
+    end.
+
+
+get_job(#{jtx := true} = JTx, Type, JobId) ->
+    #{tx := Tx, jobs_path :=  JobsPath} = get_jtx(JTx),
+    Key = pack({?DATA, Type, JobId}, JobsPath),
+    case get_job(Tx, Key) of
+        {_, WorkerLockId, Priority, JobOpts} ->
+            {ok, JobOpts, job_state(WorkerLockId, Priority)};
+        not_found ->
+            not_found
+    end.
+
+
+% Worker public API
+
+accept(#{jtx := true} = JTx, Type) ->
+    accept(JTx, Type, undefined).
+
+
+accept(#{jtx := true} = JTx0, Type, MaxPriority) ->
+    #{tx := Tx, jobs_path :=  JobsPath} = JTx = get_jtx(JTx0),
+    PendingPrefix = pack({?PENDING, Type}, JobsPath),
+    WorkerLockId = fabric2_util:uuid(),
+    % Dequeue item from front of queue (use MaxPriority) for get_range
+    % Create activity entry
+    % Update sequence if jobs table
+    % Update sequence in "watches"
+    {ok, <<"AJobId">>,  WorkerLockId}.
+
+
+
+finish(#{jtx := true} = JTx0, Type, JobId, JobOpts, WorkerLockId) ->
+    #{tx := Tx, jobs_path :=  JobsPath} = JTx = get_jtx(JTx0),
+    Key = pack({?DATA, Type, JobId}, JobsPath),
+    try
+        {Seq, _, _, JobOpts} = get_job_or_raise_status(Tx, Key, WorkerLockId),
+        ActivityKey = pack({?ACTIVITY, Type, Seq}, JobsPath),
+        erlfdb:clear(Tx, ActivityKey),
+        case maps:get(?OPT_RESUBMIT, JobOpts, false) of
+            true ->
+                Priority = maps:get(?OPT_PRIORITY, JobOpts, ?UNSET_VS),
+                JobOpts1 = maps:without([?OPT_PRIORITY], JobOpts),
+                Val = pack({null, null, Priority, jiffy:encode(JobOpts1)}),
+                case has_versionstamp(Priority) of
+                    true -> erlfdb:set_versionstamped_value(Tx, Key, Val);
+                    false -> erlfdb:set(Tx, Key, Val)
+                end,
+                pending_enqueue(JTx, Type, Priority, JobId);
+            false ->
+                Val = pack({null, null, null, jiffy:encode(JobOpts)}),
+                erlfdb:set(Tx, Key, Val)
+        end
+    catch
+        throw:worker_conflict -> worker_conflict;
+        throw:canceled -> canceled
+    end.
+
+
+resubmit(#{jtx := true} = JTx, Type, JobId, WorkerLockId) ->
+    #{tx := Tx, jobs_path :=  JobsPath} = get_jtx(JTx),
+    Key = pack({?DATA, Type, JobId}, JobsPath),
+    try
+        case get_job_or_raise_status(Tx, Key, WorkerLockId) of
+            {_, _, _, #{?OPT_RESUBMIT := true}} ->
+                ok;
+            {Seq, WorkerLockId, Priority, #{} = JobOpts} ->
+                set_resubmit(Tx, Key, Seq, WorkerLockId, Priority, JobOpts)
+        end
+    catch
+        throw:worker_conflict -> worker_conflict;
+        throw:canceled -> canceled
+    end.
+
+
+update(#{jtx := true} = JTx, Type, JobId, JobOpts, WorkerLockId) ->
+    #{tx := Tx, jobs_path :=  JobsPath} = get_jtx(JTx),
+    % TODO
+    ok. % worker_conflict | canceled
+
+
+% Cache initialization API. Called from the supervisor just to create the ETS
+% table. It returns `ignore` to tell supervisor it won't actually start any
+% process, which is what we want here.
+%
+init_cache() ->
+    ConcurrencyOpts = [{read_concurrency, true}, {write_concurrency, true}],
+    ets:new(?MODULE, [public, named_table] ++ ConcurrencyOpts),
+    ignore.
+
+
+% Cached job transaction object. This object wraps a transaction, caches the
+% directory lookup path, and the metadata version. The function can be used from
+% or outside the transaction. When used from a transaction it will verify if
+% the metadata was changed, and will refresh automatically.
+%
+get_jtx() ->
+    get_jtx(undefined).
+
+
+get_jtx(#{tx := Tx} = _TxDb) ->
+    get_jtx(Tx);
+
+get_jtx(undefined = _Tx) ->
+    case ets:lookup(?MODULE, ?JOBS) of
+        [{_, #{} = JTx}] -> JTx;
+        [] -> update_jtx_cache(init_jtx(undefined))
+    end;
+
+get_jtx({erlfdb_transaction, _} = Tx) ->
+    case ets:lookup(?MODULE, ?JOBS) of
+        [{_, #{} = JTx}] -> ensure_current(JTx#{tx := Tx});
+        [] -> update_jtx_cache(init_jtx(Tx))
+    end.
+
+
+% Private API helper functions
+
+
+get_job(Tx = {erlfdb_transaction, _}, Key) ->
+    case erlfdb:wait(erlfdb:get(Tx, Key)) of
+        <<_/binary>> = Val ->
+            {Seq, WorkerLockId, JobOptsBin} = unpack(Val),
+            JobOpts = jiffy:decode(JobOptsBin, [return_maps]),
+            {Seq, WorkerLockId, JobOpts};
+        not_found ->
+            not_found
+    end.
+
+
+get_job_or_raise_status(Tx, Key, WorkerLockId) ->
+    case get_job(Tx, Key) of
+        {_, CurWorkerLockId, _, _} when WorkerLockId =/= CurWorkerLockId ->
+            throw(worker_conflict);
+        {_, _, _, #{?OPT_CANCEL := true}} ->
+            throw(canceled);
+        {_, _, _, #{}} = Res ->
+            Res
+    end.
+
+
+set_resubmit(Tx, Key, Seq, WorkerLockId, Priority, JobOpts0) ->
+    JobOpts = JobOpts0#{?OPT_RESUBMIT => true},
+    Val = pack({Seq, WorkerLockId, Priority, jiffy:encode(JobOpts)}),
+    erlfdb:set(Tx, Key, Val).
+
+
+pending_enqueue(#{jtx := true} = JTx, Type, Priority, JobId) ->
+    #{tx := Tx, jobs_path := JobsPath} = JTx,
+    Key = pack({?PENDING, Type, Priority, JobId}, JobsPath),
+    case has_versionstamp(Priority) of
+        true -> erlfdb:set_versionstamped_key(Tx, Key, null);
+        false -> erlfdb:set(Tx, Key, null)
+    end.
+
+pending_remove(#{jtx := true} = JTx, Type, Priority, JobId) ->
+    #{tx := Tx, jobs_path := JobsPath} = JTx,
+    Key = pack({?PENDING, Type, Priority, JobId}, JobsPath),
+    erlfdb:clear(Tx, Key).
+
+
+has_versionstamp(?UNSET_VS) ->
+    true;
+
+has_versionstamp(Tuple) when is_tuple(Tuple) ->
+    has_versionstamp(tuple_to_list(Tuple));
+
+has_versionstamp([Elem | Rest]) ->
+    has_versionstamp(Elem) orelse has_versionstamp(Rest);
+
+has_versionstamp(_Other) ->
+    false.
+
+
+job_state(WorkerLockId, Priority) ->
+    case {WorkerLockId, Priority} of
+        {null, null} ->
+            finished;
+        {WorkerLockId, _} when WorkerLockId =/= null ->
+            running;
+        {_, Priority} when Priority =/= null ->
+            pending;
+        ErrorState ->
+            error({invalid_job_state, ErrorState})
+    end.
+
+
+pack(Val) ->
+    erlfdb_tuple:pack(Val).
+
+
+pack(Val, Prefix) ->
+    erlfdb_tuple:pack(Val, Prefix).
+
+
+unpack(Val) ->
+    erlfdb_tuple:unpack(Val).
+
+
+unpack(Val, Prefix) ->
+    erlfdb_tuple:unpack(Val, Prefix).
+
+
+tx(Fun) when is_function(Fun, 1) ->
+    fabric2_fdb:transactional(Fun).
+
+
+tx(Tx, Fun) when is_function(Fun, 1) ->
+    fabric2_fdb:transactional(Tx, Fun).
+
+
+% This a transaction context object similar to the Db = #{} one from fabric2_fdb.
+% It's is used to cache the jobs path directory (to avoid extra lookups on every
+% operation) and to check for metadata changes (in case directory changes).
+%
+init_jtx(undefined) ->
+    tx(fun(Tx) -> init_jtx(Tx) end);
+
+init_jtx({erlfdb_transaction, _} = Tx) ->
+    Root = erlfdb_directory:root(),
+    CouchDB = erlfdb_directory:create_or_open(Tx, Root, [<<"couchdb">>]),
+    LayerPrefix = erlfdb_directory:get_name(CouchDB),
+    JobsPrefix = erlfdb_tuple:pack({?JOBS}, LayerPrefix),
+    Version = erlfdb:wait(erlfdb:get(Tx, ?METADATA_VERSION_KEY)),
+    % layer_prefix, md_version and tx here match db map fields in fabric2_fdb
+    % but we also assert that this is a job transaction using the jtx => true field
+    #{
+        jtx => true,
+        tx => Tx,
+        layer_prefix => LayerPrefix,
+        jobs_prefix => JobsPrefix,
+        md_version => Version
+    }.
+
+
+ensure_current(#{jtx := true, tx := Tx, md_version := Version} = JTx) ->
+    case erlfdb:wait(erlfdb:get(Tx, ?METADATA_VERSION_KEY)) of
+        Version -> JTx;
+        _NewVersion -> update_jtx_cache(init_jtx(Tx))
+    end.
+
+
+update_jtx_cache(#{jtx := true} = JTx) ->
+    CachedJTx = JTx#{tx := undefined},
+    ets:insert(?MODULE, {?JOBS, CachedJTx}),
+    JTx.
diff --git a/src/couch_jobs/src/couch_jobs_sup.erl b/src/couch_jobs/src/couch_jobs_sup.erl
new file mode 100644
index 0000000..8a7832a
--- /dev/null
+++ b/src/couch_jobs/src/couch_jobs_sup.erl
@@ -0,0 +1,46 @@
+%
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_jobs_sup).
+
+
+-behaviour(supervisor).
+
+
+-export([
+    start_link/0
+]).
+
+-export([
+    init/1
+]).
+
+
+start_link() ->
+    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+
+init([]) ->
+    Flags = #{
+        strategy => one_for_one,
+        intensity => 1,
+        period => 5
+    },
+    Children = [
+        #{
+            id => couch_jobs_fdb,
+            restart => transient,
+            start => {couch_jobs_fdb, init_cache, []}
+        }
+    ],
+    {ok, {Flags, Children}}.