077e542c1a
Каждый рендер «Настройки» ждал /section службы bj-quikexport, а та считала min(ts) и count(*) за сутки по npr.history (18,5 млн строк, индекса по ts нет): 6 + 27 с на 28, срез по таймауту 10 с. Теперь начало истории — первая строка по id, строки за сутки/час — двоичный поиск по id (история только дописывается): ~45 мс. То же в /admin/npr/api/freshness. bj-server ждёт раздел QUIK-экспорта не дольше 3 с и иначе отдаёт последний удачный; прогрев версий, пользователей ЛК и раздела после запуска. «Обновления» перезагружают страницу, только если что-то изменилось. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
156 lines
5.9 KiB
Go
156 lines
5.9 KiB
Go
package lkgateway
|
||
|
||
// Модуль «Экспорт биржевой информации» (ARQA quikexport) работает отдельной службой bj-quikexport
|
||
// (modules/quikexport, обновляется независимо). Раздел остаётся на странице «Настройка»: bj-server
|
||
// получает от службы готовый HTML раздела и цвет индикатора меню, а формы раздела
|
||
// (/admin/setup/quikexport/…) проксирует службе и превращает её ответ X-BJ-Flash в обычное
|
||
// сообщение страницы настроек. Лицензию проверяет bj-server и передаёт в X-BJ-Licensed.
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
"html/template"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// quikExportFeature — лицензионный признак модуля.
|
||
const quikExportFeature = "quik-export"
|
||
|
||
// Применение настроек ждёт root-воркер до 3 минут.
|
||
var quikExportHTTP = &http.Client{Timeout: 4 * time.Minute, CheckRedirect: noRedirect}
|
||
|
||
func quikExportURL() string {
|
||
if u := strings.TrimSpace(os.Getenv("BJ_QUIKEXPORT_URL")); u != "" {
|
||
return strings.TrimRight(u, "/")
|
||
}
|
||
return "http://127.0.0.1:8084"
|
||
}
|
||
|
||
// QuikExportView — раздел «Экспорт QUIK» на странице настроек.
|
||
type QuikExportView struct {
|
||
HTML template.HTML
|
||
Ind string // индикатор в меню: ok | warn | err | пусто
|
||
}
|
||
|
||
func quikExportDo(ctx context.Context, rc *RuntimeConfig, method, uri string, body io.Reader, contentType string) (*http.Response, error) {
|
||
key, err := moduleKey()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("служебный ключ модулей: %w", err)
|
||
}
|
||
req, err := http.NewRequestWithContext(ctx, method, quikExportURL()+uri, body)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if contentType != "" {
|
||
req.Header.Set("Content-Type", contentType)
|
||
}
|
||
req.Header.Set("X-BJ-Module-Key", key)
|
||
lic := "0"
|
||
if moduleAllowed(rc, quikExportFeature) {
|
||
lic = "1"
|
||
}
|
||
req.Header.Set("X-BJ-Licensed", lic)
|
||
return quikExportHTTP.Do(req)
|
||
}
|
||
|
||
// qxSection — последний ответ службы: страница «Настройка» не ждёт её дольше qxSectionWait, а отдаёт
|
||
// последний удачный раздел (служба могла задуматься над статистикой базы).
|
||
var qxSection struct {
|
||
sync.Mutex
|
||
v QuikExportView
|
||
at time.Time
|
||
ro bool
|
||
}
|
||
|
||
const qxSectionWait = 3 * time.Second
|
||
|
||
func quikExportView(rc *RuntimeConfig, showRO bool) QuikExportView {
|
||
uri := "/section"
|
||
if showRO {
|
||
uri += "?reveal_ro=1"
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), qxSectionWait)
|
||
defer cancel()
|
||
resp, err := quikExportDo(ctx, rc, http.MethodGet, uri, nil, "")
|
||
if err != nil {
|
||
qxSection.Lock()
|
||
v, at, ro := qxSection.v, qxSection.at, qxSection.ro
|
||
qxSection.Unlock()
|
||
if !at.IsZero() && ro == showRO && time.Since(at) < 30*time.Minute {
|
||
return v
|
||
}
|
||
}
|
||
if err == nil {
|
||
b, rerr := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||
resp.Body.Close()
|
||
switch {
|
||
case rerr != nil:
|
||
err = rerr
|
||
case resp.StatusCode != http.StatusOK:
|
||
err = fmt.Errorf("служба ответила %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
|
||
default:
|
||
v := QuikExportView{HTML: template.HTML(b), Ind: resp.Header.Get("X-BJ-Indicator")}
|
||
qxSection.Lock()
|
||
qxSection.v, qxSection.at, qxSection.ro = v, time.Now(), showRO
|
||
qxSection.Unlock()
|
||
return v
|
||
}
|
||
}
|
||
return QuikExportView{Ind: "err", HTML: template.HTML(`<h1>QUIK — Экспорт биржевой информации</h1>`) + moduleMessage(
|
||
"Служба модуля не отвечает",
|
||
"Служба bj-quikexport не запущена или ещё не установлена на этом сервере ("+err.Error()+").",
|
||
"Установка и обновление модуля: Настройка → Обновления. Состояние службы: systemctl status bj-quikexport. Сам экспортёр ARQA от этого не зависит и продолжает выгрузку.")}
|
||
}
|
||
|
||
// registerQuikExportSetup — формы раздела проксируются службе bj-quikexport.
|
||
func registerQuikExportSetup(mux *http.ServeMux, h *setupHandlers) {
|
||
proxy := func(w http.ResponseWriter, r *http.Request) {
|
||
body, err := io.ReadAll(io.LimitReader(r.Body, 8<<20))
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
if len(body) == 0 && r.PostForm != nil && !strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/") {
|
||
body = []byte(r.PostForm.Encode()) // форму уже разобрали раньше
|
||
}
|
||
// setupFlash читает return_to/section из формы — возвращаем тело на место.
|
||
restore := func() { r.Body = io.NopCloser(bytes.NewReader(body)) }
|
||
resp, err := quikExportDo(r.Context(), h.rc, r.Method, r.URL.RequestURI(), bytes.NewReader(body), r.Header.Get("Content-Type"))
|
||
if err != nil {
|
||
if r.Method == http.MethodPost {
|
||
restore()
|
||
setupFlash(w, r, "Экспорт QUIK: служба bj-quikexport не отвечает — "+err.Error())
|
||
return
|
||
}
|
||
http.Error(w, "служба bj-quikexport не отвечает: "+err.Error(), http.StatusServiceUnavailable)
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
if f := resp.Header.Get("X-BJ-Flash"); f != "" {
|
||
msg, uerr := url.PathUnescape(f)
|
||
if uerr != nil {
|
||
msg = f
|
||
}
|
||
restore()
|
||
setupFlash(w, r, msg)
|
||
return
|
||
}
|
||
for _, k := range []string{"Content-Type", "Content-Disposition", "Cache-Control"} {
|
||
if v := resp.Header.Get(k); v != "" {
|
||
w.Header().Set(k, v)
|
||
}
|
||
}
|
||
w.WriteHeader(resp.StatusCode)
|
||
_, _ = io.Copy(w, resp.Body)
|
||
}
|
||
mux.HandleFunc("/admin/setup/quikexport", proxy)
|
||
mux.HandleFunc("/admin/setup/quikexport/", proxy)
|
||
}
|