Files
Bridge-and-Join-s/internal/lkgateway/quikexport_proxy.go
T
fontvielle 077e542c1a «Настройка» без 10-секундного ожидания: статистика npr.history по первичному ключу
Каждый рендер «Настройки» ждал /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
2026-10-22 11:01:45 +03:00

156 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}