Fosi Fossil Service
Small teams. Open source. Built on Fossil.

CaaS / Lamina

Unified Management for Fossil Repositories

Public | Owner: CaaS | Default branch: trunk | Last update: 2026-10-25 00:41

Artifact 22f7afc3e4faee9f32cd9478e5325e0231fd0243a6545b5dfc425a7a6cf399dc:


;; 仓库进程管理器:批量启动仓库、处理 fossil server 退出并有限重试。
(defmodule lamina_repo_mgr
  (behaviour gen_server)
  (export all))

(defun max-restarts () 3)

(defun repo-key (org-name slug) (tuple org-name slug))

(defun start-link ()
  (gen_server:start_link #(local lamina_repo_mgr)
                         'lamina_repo_mgr 'none (list)))

(defun start_link () (start-link))

(defun start-repo (slug fossil-path)
  (start-repo 'none slug fossil-path))

(defun start-repo (org-name slug fossil-path)
  (gen_server:call 'lamina_repo_mgr
                   (tuple 'start (normalize-org org-name) slug fossil-path) 30000))

(defun stop-repo (slug)
  (stop-repo 'none slug))

(defun stop-repo (org-name slug)
  (gen_server:call 'lamina_repo_mgr
                   (tuple 'stop (normalize-org org-name) slug) 30000))

(defun status (slug)
  (status 'none slug))

(defun status (org-name slug)
  (gen_server:call 'lamina_repo_mgr
                   (tuple 'status (normalize-org org-name) slug) 5000))

(defun init (_)
  (process_flag 'trap_exit 'true)
  (erlang:send_after 0 (self) 'start-all)
  `#(ok #M()))

(defun handle_call (msg _from state)
  (case msg
    (`#(start ,org-name ,slug ,fossil-path)
     (let ((key (repo-key org-name slug)))
       (case (maps:find key state)
         (`#(ok #(,pid ,_path ,_failures))
          (if (and (is_pid pid) (is_process_alive pid))
              (tuple 'reply 'ok state)
              (start-and-reply org-name slug fossil-path state)))
         ('error (start-and-reply org-name slug fossil-path state)))))
    (`#(stop ,org-name ,slug)
     (let ((key (repo-key org-name slug)))
       (case (maps:find key state)
         (`#(ok #(,pid ,_path ,_failures))
          (if (is_pid pid) (catch (gen_server:stop pid 'normal 3000)) 'ok)
          (lamina_repo_registry:delete org-name slug)
          (tuple 'reply 'ok (maps:remove key state)))
         ('error (tuple 'reply 'ok state)))))
    (`#(status ,org-name ,slug)
     (let ((key (repo-key org-name slug)))
       (case (maps:find key state)
         (`#(ok #(,pid ,_path ,failures))
          (if (and (is_pid pid) (is_process_alive pid))
              `#(reply #(ok up ,pid) ,state)
              `#(reply #(ok down ,failures) ,state)))
         ('error (tuple 'reply 'undefined state)))))
    (_ `#(reply 'unknown ,state))))

(defun handle_info (msg state)
  (case msg
    ('start-all
     (start-all state))
    (`#(EXIT ,pid ,reason)
     (case (find-key-by-pid pid state)
       ('none `#(noreply ,state))
       (key
        (let (((tuple org-name slug) key))
          (let (((tuple _old-pid fossil-path failures) (maps:get key state)))
            (logger:warning "repo_server ~s/~s exited: ~p"
                            (list org-name slug reason))
            (let ((state1 (maps:put key (tuple 'undefined fossil-path failures) state)))
              (schedule-restart org-name slug fossil-path failures state1)))))))
    (`#(restart ,org-name ,slug ,fossil-path ,failures)
     (case (start-child org-name slug fossil-path)
       (`#(ok ,pid)
        (set-repo-status org-name slug "up")
        `#(noreply ,(maps:put (repo-key org-name slug)
                              (tuple pid fossil-path failures) state)))
       (`#(error ,reason)
        (logger:error "repo_server ~s/~s restart failed: ~p"
                      (list org-name slug reason))
        (schedule-restart org-name slug fossil-path failures state))))
    (_ `#(noreply ,state))))

(defun start-all (state)
  (case (catch (lamina_db:q
                "SELECT COALESCE(o.name,''), r.name, r.fossil_path FROM repositories r LEFT JOIN organizations o ON o.id=r.organization_id;"))
    (`((,org-name ,name ,path) . ,rest)
     (let ((state1 (start-one (normalize-org org-name) (to-str name) (to-str path) state)))
       (start-rows rest state1)))
    (_ `#(noreply ,state))))

(defun start-rows (rows state)
  (case rows
    ('() `#(noreply ,state))
    (`((,org-name ,name ,path) . ,rest)
     (start-rows rest
                 (start-one (normalize-org org-name) (to-str name) (to-str path) state)))))

(defun start-one (org-name slug path state)
  (let ((key (repo-key org-name slug)))
    (case (maps:is_key key state)
      ('true state)
      ('false
       (case (start-child org-name slug (to-str path))
         (`#(ok ,pid)
          (set-repo-status org-name slug "up")
          (maps:put key (tuple pid path 0) state))
       (`#(error ,reason)
          (logger:error "repo_server ~s/~s initial start failed: ~p"
                        (list org-name slug reason))
          (erlang:send_after 1000 (self)
                             (tuple 'restart org-name slug path 1))
          (set-repo-status org-name slug "starting")
          (maps:put key (tuple 'undefined path 1) state)))))))

(defun start-and-reply (org-name slug path state)
  (case (start-child org-name slug path)
    (`#(ok ,pid)
     (set-repo-status org-name slug "up")
     (tuple 'reply 'ok
            (maps:put (repo-key org-name slug) (tuple pid path 0) state)))
    (`#(error ,reason)
     (set-repo-status org-name slug "down")
     `#(reply #(error ,reason) ,state))))

(defun schedule-restart (org-name slug path failures state)
  (let ((next (+ failures 1)))
      (if (=< next (max-restarts))
        (progn
          (set-repo-status org-name slug "starting")
          (erlang:send_after (* 1000 next) (self)
                             (tuple 'restart org-name slug path next))
          `#(noreply ,(maps:put (repo-key org-name slug)
                                (tuple 'undefined path next) state)))
        (progn
          (logger:error "repo_server ~s/~s repeatedly failed; giving up"
                        (list org-name slug))
          (set-repo-status org-name slug "down")
          `#(noreply ,(maps:put (repo-key org-name slug)
                                (tuple 'undefined path next) state))))))

(defun set-repo-status (org-name slug status)
  (case (normalize-org org-name)
    ('none
     (catch (lamina_db:q
             "UPDATE repositories SET repo_status=? WHERE name=? AND organization_id IS NULL;"
             (list status slug))))
    (org
     (catch (lamina_db:q
             "UPDATE repositories SET repo_status=? WHERE name=? AND organization_id=(SELECT id FROM organizations WHERE name=?);"
             (list status slug org))))))

(defun start-child (org-name slug fossil-path)
  (case (catch (lamina_repo_server:start-link org-name slug fossil-path))
    (`#(ok ,pid) `#(ok ,pid))
    (`#(error ,reason) `#(error ,reason))
    (`#(EXIT ,reason) `#(error ,reason))
    (other `#(error ,other))))

(defun find-key-by-pid (wanted-pid state)
  (let ((matches
         (lists:filter
          (lambda (entry)
            (let (((tuple _key value) entry))
              (let (((tuple entry-pid _path _failures) value))
                (=:= wanted-pid entry-pid))))
          (maps:to_list state))))
    (case matches
      ('() 'none)
      (`(#(,key ,_) . ,_) key))))

(defun terminate (_reason state)
  (lists:foreach
   (lambda (entry)
     (let (((tuple key value) entry))
       (let (((tuple org-name slug) key))
         (let (((tuple pid _path _failures) value))
           (if (is_pid pid) (catch (gen_server:stop pid 'normal 3000)) 'ok)
           (lamina_repo_registry:delete org-name slug)))))
   (maps:to_list state))
  'ok)

(defun code-change (_old state _extra) `#(ok ,state))

(defun normalize-org (org-name)
  (cond
    ((or (=:= org-name 'none) (=:= org-name 'undefined) (=:= org-name 'null)) 'none)
    ((is_binary org-name) (normalize-org (binary_to_list org-name)))
    ((or (=:= org-name "") (=:= org-name #"")) 'none)
    ('true org-name)))

(defun to-str (v)
  (if (is_binary v) (binary_to_list v) v))

Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/CaaS/Lamina

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.