•  cahaya takoreh

Membina pengagregat kandungan untuk keseronokan dan keuntungan?

Apl penuh yang mengikis, memproses dan membentangkan kandungan daripada web...di web.

kenapa?

Maklumat yang berlebihan ? Ia agak teruk hari ini, banyak isyarat rendah kepada sumber maklumat yang bising, mengecilkan "suapan" anda supaya anda tidak terharu adalah agak sukar. Alat yang menapis maklumat dan membentangkannya dalam format yang mudah dan cepat dihadam akan sangat berguna. Inilah sebabnya saya pertimbangkan pengagregatan kandungan padang malar hijau untuk gangguan. Ia adalah dan akan sentiasa (selagi internet percuma dan ada kebebasan bersuara) peluang perniagaan yang baik. Ia adalah salah satu contoh di mana semuanya mengenai pelaksanaan (dan tidak ada sama sekali mengenai idea itu).

Menguruskan jangkaan

Setelah berkata demikian, aplikasi saya pada akhirnya tidak melakukan apa-apa penapisan sebenar. Sebenarnya ia secara ringkas agregat kandungan daripada web. Ini kerana saya tidak membina pengguna ke dalamnya, dan terdapat sedikit insentif untuk melakukan penapisan jika ia tidak boleh disesuaikan bagi setiap pengguna.

Seni bina

Gambar rajah seni bina: rajah-pengumpul kandunganMemang terdapat banyak kalangan di dalamnya!...anda tahu...mikro...perkhidmatan? Aplikasi yang saya bina adalah "pengikis" dan "pelayan", manakala "penerbit" hanyalah rutin yang tertanam dalam pelayan. "Carian" dan "proksi" ialah alat luaran yang menjalankan tugas mereka. "Frontend" bukanlah sesuatu yang istimewa, gabungan js dan css yang digabungkan dengan webpack.

Oleh kerana terdapat bahagian bergerak yang berbeza saya akan mengikut aliran kandungan, bermula dari apabila kandungan dilihat buat kali pertama.

Pengikis

Mengikis sudah selesai...kau rasa, ular sawa. Walau bagaimanapun, tiada modul "mengikis" ad hoc digunakan.

Apa yang anda kikis?

Memutuskan perkara yang hendak dikikis bergantung pada kategori kandungan. Kami memanggil kategori "topik".

Mengikis berlaku secara berterusan, ia adalah syaitan. Kod pseudo gelung utama kelihatan seperti ini, dan ia dikonfigurasikan setiap tapak:

Penerbitan

Sebaik sahaja kami mempunyai kandungan untuk diterbitkan, kami perlu memutuskan perkara yang hendak diterbitkan dan berapa kerap. Saya tidak membuat sebarang helah di sini, kerana seperti yang saya nyatakan sebelum ini memilih apa yang hendak dipaparkan adalah bergantung kepada pengguna. Jadi kami hanya menerbitkan daripada terbaru kepada tertua , dengan alasan bahawa sesuatu yang kami kikis baru-baru ini adalah lebih relevan, ia adalah a LIFO beratur. Penerbitan walaupun agak bergantung pada alat python, berlaku dalam nim, kerana ia berjalan bersebelahan dengan pelayan, yang juga dalam nim.

Logik penerbitan

Penerbitan berlaku secara berterusan, dan seperti mengikis mempunyai selang terbiar, penerbitan berlaku, secara songsang kepada mengikis. Apabila kita mengikis, kita perlahan apabila kita mempunyai a cukup panjang cache artikel yang tidak diterbitkan, dengan penerbitan kami perlahan apabila cache kami mula mengecut terlalu banyak. Dengan cara ini, ditambah dengan mengikis, perlu sentiasa ada beberapa kandungan untuk diterbitkan beberapa waktu pada masa hadapan. Logik penerbitan sebenar:

Melayan

Kami menyediakan kerja untuk mengikis kandungan penerbitan, yang tinggal hanyalah menyajikannya.

Pelayan web

Selepas mempunyai mencuba berbeza web pelayan, kerana pepijat yang berbeza yang saya selesaikan pengukir.

Mengendalikan permintaan

Penghala

Kami menggunakan nim pelaksanaan padanan corak untuk memadankan sekumpulan tangkapan regex. Ini adalah regex yang tidak RESTful sama sekali:

const
  rxend = "(?=/+|(?=[?].*)|$)"
  rxAmp = fmt"(/+amp{rxend})"
  rxLang = "(/[a-z]{2}(?:-[A-Z]{2})?" & fmt"{rxend})" # split to avoid formatting regex `{}` usage
  rxTopic = fmt"(/+.*?{rxend})"
  rxPage = fmt"(/+(?:[0-9]+|s|g|feed\.xml|sitemap\.xml){rxend})"
  rxArt = fmt"(/+.*?{rxend})"
  rxPath = fmt"{rxAmp}?{rxLang}?{rxTopic}?{rxPage}?{rxArt}?"

rxPath menunjukkan semua kemungkinan nod yang boleh dimiliki oleh laluan. Kemudian penghalaan kami kelihatan seperti:

let capts = uriTuple(reqCtx.url.path)
case capts:
  of (topic: ""): # homepage...
  of (topic: "assets"): # assets
  of (topic: "i"): # images
  of (topic: "robots.txt"): # robots.txt
  of (page: "sitemap.xml"): # sitemap for topics
  of (art: "index.xml"): # sitemap index for topic pages
  etc...

Ia tidak cantik, tetapi tidak bergantung pada mana-mana penghala tertentu membenarkan saya menukar pelayan web bawah tanpa banyak kekecohan semasa menguji. Adakah ia berprestasi? Tidak jelas! Tidak melakukan sebarang penanda aras membandingkannya dengan sesuatu yang lain. Walau bagaimanapun, yang berbau adalah regex yang boleh mempunyai pepijat, dan hakikat bahawa pesanan kes itu penting.

Tunggu sekejap...

Terdapat banyak perkara yang kami lakukan pada setiap permintaan sebelum penghalaan halaman sebenar:

pada mulanya kami menyediakan kod pembersihan (dengandefer: ) yang semestinya pastikan tiada kebocoran berlaku.

defer:
  # FIXME: is this cleanup required?
  var futs: seq[Future[void]]
  let resp =
    if ctx.response.issome: ctx.response.get
    else: nil
  if not resp.isnil and not resp.connection.isnil:
    futs.add resp.connection.closeWait()
  if not ctx.isnil:
    if not ctx.connection.isnil:
      futs.add ctx.connection.closeWait()
    futs.add ctx.closeWait()
  await allFutures(futs)

Kami menyemak sama ada benang dimulakan:

initThread()

Ini sepatutnya hanya dijalankan sekali sahaja (ia menetapkan bool global selepas pemulaan untuk menyemak), dan boleh dilakukan di luar pengendali permintaan. Tetapi apa yang sebenarnya dimulakan? Nah...barang yang agak banyak! Pada asasnya kami (ab) menggunakan pemalar global yang memerlukan permulaan, juga sebahagian daripada ini tidak benar-benar berkaitan dengan benang, kerana ia memulakan ingatan pada timbunan dan dikongsi merentas utas

if threadInitialized:
  debug "thread: already initialized"
  return
debug "thread: base"
initThreadBase()
debug "thread: sonic"
initSonic() # Must be on top
debug "thread: http"
initHttp()
debug "thread: html"
initHtml()
debug "thread: ldj"
initLDJ()
debug "thread: feed"
initFeed()
debug "thread: img"
startImgFlow()
debug "thread: lsh"
startLsh()
debug "thread: mimes"
initMimes()
# ... and other stuff

Kemudian kami menghuraikan parameter

var
  relpath = ctx.rawPath
  page: string
  rqlocked: bool
relpath.removeSuffix('/')
debug "handling: {relpath:.120}"

handleParams()

Untuk apa kita menggunakan parameter? TheParamKey jenis enum menerangkannya:

type
  ParamKey = enum
    none,
    q, p, # sonic
    c, # cache
    d, # delete
    t,  # translations
    u # imgUrls

Kami buat microcaching untuk permintaan supaya setiap permintaan dicache mengikut tupel(path, query, accetEncoding) , pengekodan diperlukan kerana kami boleh melayani kedua-dua badan (bukan)mampat. Konteks permintaan kelihatan seperti ini:

let reqCtx = reqCtxCache.lcheckOrPut(reqCacheKey):
  let reqCtx {.gensym.} = new(ReqContext)
  block:
    let l = newAsyncLock()
    checkNil(l):
      reqCtx.lock = l
  reqCtx.url = move url
  reqCtx.params = params
  reqCtx.file = reqCtx.url.path.fp
  reqCtx.key = hash(reqCtx.file)
  reqCtx.rq = initTable[ReqId, HttpRequestRef]()
  new(reqCtx.respBody)
  reqCtx
if handleTranslation():
  return

Mengapa ini juga dilakukan sebelum penghalaan? Secara lalai, kami menyediakan halaman yang diterjemahkan separa. Kami miskin :( dan terjemahan adalah berdasarkan perkhidmatan percuma, tetapi kami tidak mampu membayar masa pemuatan yang teruk, jadi kami menjalankan terjemahan tertunda semasa kami menyediakan halaman yang diterjemahkan dengan hanya coretan yang telah dicache dalam pangkalan data terjemahan kami.

Pada ketika ini terdapat penghalaan, dibalut dengan pengecualian supaya jika penyajian halaman yang betul gagal, kami mengeluarkan503 . Mengeluarkan a503 membayangkan bahawa kami cuba menghalakan url yang sah tetapi kami tidak dapat menjana halaman. Untuk url yang tidak sah kami mengeluarkan a301 ubah hala yang menunjukkan bahawa url itu tidak sah. Kami menyediakan 11 jenis url yang berbeza:

Mengenai rendering

Paparan halaman diproses dari sisi nim menggunakan karax

Susun atur halaman umum

Laman web ini terdiri daripada bar tetap teratas yang menunjukkan:

Memandangkan ia adalah reka bentuk responsif, apabila port pandang lebih kecil, bar atas hanya memegang kotak carian manakala selebihnya muncul dalam bar sisi boleh togol.

RSS

Ini adalah contoh fungsi, menunjukkan perkara yang kami lakukan apabila siaran baharu diterbitkan untuk mengemas kini suapan:

proc update*(tfeed: Feed, topic: string, newArts: seq[Article], dowrite = false) =
    ## Load existing feed for given topic and update the feed (in-memory)
    ## with the new articles provided, it does not write to storage.
    checkNil tfeed
    let
        chann = tfeed.findel("channel")
        itms = chann.drainChannel
        arl = itms.len
        narl = newArts.len

    debug "rss: newArts: {narl}, previous: {arl}"
    let
        fill = RSS_N_ITEMS - arl
        rem = max(0, narl - fill)
        shrinked = if (rem > 0 and arl > 0):
                       itms[0..<(max(0, arl-rem))]
                   else: itms
    debug "rss: articles tail len {len(shrinked)}, newarts: {len(newArts)}"
    assert shrinked.len + narl <= RSS_N_ITEMS, fmt"shrinked: {shrinked.len}, newarticles: {narl}"
    for a in newArts:
        chann.add articleItem(a)
    for itm in shrinked:
        chann.add itm
    if dowrite:
   
        pageCache[][topic.feedKey] = tfeed.toXmlString

Peta laman

Ini adalah teras menambah url pada peta laman:

template addUrlToFeed(getLoc, getLocLang) =
  if unlikely(nEntries > maxEntries):
      warn "Number of URLs for sitemap of topic: {topic} exceeds limit! {nEntries}/{maxEntries}"
      break
  let
      url = newElement("url")
      loc = newElement("loc")
  loc.add getLoc().escape.newText
  url.add loc
  addLangs(url, getLocLang)
  result.add url

proc buildTopicPagesSitemap*(topic: string): Future[XmlNode] {.async.} =
    initSitemapIndex()
    await syncTopics()
    var nEntries = 0
    let done = await topicDonePages(topic)
    template langUrl(lang): untyped {.dirty.} = $(WEBSITE_URL / lang / topic / pages[n])
    withPyLock:
        # add the most recent articles first (pages with higher idx)
        let pages = pybi[].list(done.keys()).to(seq[string])
        for n in countDown(pages.len - 1, 0):
          if not (await isEmptyPage(topic, pages[n].parseInt, false)):
            discard sitemapUrl(topic, pages[n]).sitemapEl

template addArticleToFeed() =
  template baseUrl(): untyped =
    getArticleUrl(a, topic)

  template langUrl(lang): untyped =
    getArticleUrl(a, topic, lang)

  if not a.isValidArticlePy:
      continue

  addUrlToFeed(baseUrl, langUrl)

proc buildTopicSitemap(topic: string): Future[XmlNode] {.async.} =
    initUrlSet()
    await syncTopics()
    let done = await topicDonePages(topic)
    var nEntries = 0
    withPyLock:
        # add the most recent articles first (pages with higher idx)
        for pagenum in countDown(len(done) - 1, 0):
            if unlikely(nEntries > maxEntries):
                warn "Number of URLs for sitemap of topic: {topic} exceeds limit! {nEntries}/{maxEntries}"
                break
            checkTrue pagenum in done, "Mismatching number of pages"
            for a in done[pagenum]:
                addArticleToFeed()

templat

Kami tidak menggunakan enjin templat kerana kebanyakan pemaparan dilakukan dengan karax, tetapi untuk halaman seperti ToS kami menggunakan templat fail, di mana kami hanya menggantikan sekumpulan pembolehubah, sepertienvsubst perintah.

proc pageFromTemplate*(tpl, lang, amp: string): Future[string] {.async.} =
  var txt = await readfileAsync(ASSETS_PATH / "templates" / tpl & ".html")
  let (vars, title, desc) =
    case tpl:
      of "dmca": (tplRep, "DMCA", fmt"dmca compliance for {WEBSITE_DOMAIN}")
      of "tos": (ppRep, "Terms of Service",
          fmt"Terms of Service for {WEBSITE_DOMAIN}")
      of "privacy-policy": (ppRep, "Privacy Policy",
          fmt"Privacy Policy for {WEBSITE_DOMAIN}")
      else: (tplRep, tpl, "")
  txt = multiReplace(txt, vars)
  let
    slug = slugify(title)
    page = await buildPage(title = title, content = txt, wrap = true)
  checkNil(page):
    let processed = await processPage(lang, amp, page, relpath = tpl)
    checkNil(processed, fmt"failed to process template {tpl}, {lang}, {amp}"):
      return processed.asHtml(minify_css = (amp == ""))

Halaman artikel

Apabila kami memaparkan halaman seperti rumah/topik dan halaman bernombor, kami perlu menunjukkan senarai artikel, fungsi ini dipanggil dalam gelung untuk jumlah artikel yang ingin kami paparkan:

import htmlparser
proc articleEntry(ar: Article, topic = ""): Future[VNode] {.async.} =
  if ar.topic == "" and topic != "":
    ar.topic = topic
  let relpath = getArticlePath(ar)
  try:
    return buildHtml(article(class = "entry")):
      h2(class = "entry-title", id = ar.slug):
        a(href = relpath):
          text ar.title
      tdiv(class = "entry-info"):
        span(class = "entry-author"):
          text ar.getAuthor & ", "
        time(class = "entry-date", datetime = ($ar.pubDate)):
          italic:
            text format(ar.pubDate, "dd/MMM")
      tdiv(class = "entry-tags"):
        if ar.tags.len == 0:
          span(class = "entry-tag-name"):
            a(href = (await nextAdsLink()), target = "_blank"):
              icon("i-mdi-tag")
              text "none"
        else:
          for t in ar.tags:
            if likely(t.isSomething):
              span(class = "entry-tag-name"):
                a(href = (await nextAdsLink()), target = "_blank"):
                  icon("i-mdi-tag")
                  text t
      buildImgUrl(ar)
      tdiv(class = "entry-content"):
        verbatim(articleExcerpt(ar))
        a(class = "entry-more", href = relpath):
          text "[continue]"
      hr()
  except Exception as e:
    logexc()
    warn "articles: entry creation failed."
    raise e

proc buildShortPosts*(arts: seq[Article], topic = ""): Future[
    string] {.async.} =
  for a in arts:
    result.add $(await articleEntry(a, topic))

Perhatikan bagaimana dalam beberapa baris "iklan" merayap dalam X)

Senarai topik

Di bar atas kami menunjukkan senarai topik, inilah yang mencetaknya:

proc topicsList*(ucls: string; icls: string; small: static[
    bool] = true): Future[VNode] {.async.} =
  result = newVNode(VNodeKind.ul)
  result.setAttr("class", ucls)
  let topics = await loadTopics(-MENU_TOPICS) # NOTE: the sign is negative, we load the most recent N topics
  result.add buildHtml(tdiv(class = "topics-shadow"))
  var topic_slug, topic_name: string
  var isEmpty: bool
  for i in 0..<topics.len:
    withPyLock:
      (topic_slug, topic_name) = ($topics[i][0], $topics[i][1])
      isEmpty = isEmptyTopic(topic_slug)
    if isEmpty:
      continue
    let liNode = buildHtml(li(class = fmt"{icls}")):
      # tdiv(class = "mdc-icon-button__ripple") # not used without material icons
      a(href = ($(WEBSITE_URL / topic_slug)), title = topic_name,
          class = "mdc-ripple-button"):
        tdiv(class = "mdc-ripple-surface  mdc-ripple-upgraded")
        when small:
          # only use the first letter
          text $topic_name.runeAt(0).toUpper # loadTopics iterator returns pyobjects
        else:
          text topic_name
      when small:
        br()
      else:
        span(class = "separator")
    result.add liNode

Terdapat beberapa kelas reka bentuk bahan berkod keras berbau di sini. Terus terang komponen reka bentuk bahan google menyedut.

Pengaki catatan

Pengaki catatan muncul di bahagian bawah sebelah kanan halaman artikel (dalam ltr) dan ia benar-benar hanya mencetak tarikh diterbitkan.

proc postFooter(pubdate: Time): VNode =
  let dt = inZone(pubdate, utc())
  buildHtml(tdiv(class = "post-footer")):
    time(datetime = ($dt)):
      text "Published date: "
      italic:
        text format(dt, "dd MMM yyyy")

Petikan

Semasa membina entri artikel, kami mungkin memerlukan petikan jika tiada ringkasan tersedia.

proc articleExcerpt(a: Article): string =
  let alen = len(a.content) - 1
  let maxlen = min(alen, ARTICLE_EXCERPT_SIZE)
  if maxlen == alen:
    return a.content
  else:
    let runesize = runeLenAt(a.content, maxlen)
    # If article contains html tags, the excerpt might have broken html
    return parseHtml(a.content[0..maxlen+runesize]).innerText & "..."

Wtf adalahparseHtml buat di sini? Dalam kes kami membenarkan html dalam kandungan artikel (tetapi hanya beberapa teg), ini adalah pilihan daripada trafilatura modul python, yang kami kekalkan didayakan kerana ia boleh menjejaskan format artikel. Kita juga perlu berhati-hati tentang memotong rentetan utf-8...

Minifikasi

Tugas terakhir selepas membina karaxVNode pokok adalah untuk membuang bait. Pokok jika diawali dengan pengepala html, dan secara pilihan dikecilkan.

proc asHtml*(data: string ; minify: static[bool] = true; minify_css: bool = true): string =
  let html = "<!DOCTYPE html>" & "\n" & data
  sdebug "html: raw size {len(html)}"
  result = when minify:
             html.minifyHtml(minify_css = false,
                             minify_js = false,
                             keep_closing_tags = true,
                             do_not_minify_doctype = true,
                             keep_spaces_between_attributes = true,
                             ensure_spec_compliant_unquoted_attribute_values = true)
           else:
             html
  sdebug "html: minified size {len(result)}"

Minifikasi dikendalikan oleh minify-html yang kami telah terikat menggunakan c2nim , fail yang mengikat mengandungi:

proc minify*(code: cstring,
             do_not_minify_doctype = false,
             ensure_spec_compliant_unquoted_attribute_values = false,
             keep_closing_tags = true,
             keep_comments = false,
             keep_html_and_head_opening_tags = true,
             keep_spaces_between_attributes = false,
             minify_css = true,
             minify_js = true,
             remove_bangs = false,
             remove_processing_instructions = true): cstring {.importc: "minify".}

proc minifyHtml*(tree: VNode): string = $minify(($tree).cstring)
proc minifyHtml*(data: string): string = $minify(data.cstring)
template minifyHtml*(data: string, args: varargs[untyped]): string =
    $minify(data.cstring, args)

Tetapi untuk membina kita perlu menyediakan perpustakaan statik, menambah baris ini dalam kitanim.cfg

--passL:"$PROJECT_DIR/src/rust/target/release/libminify_html_c.a"

Maksud saya...itulah laluan saya apabila saya membina perpustakaan minify yang btw sebenarnya tidak mempunyai fungsi extern c yang boleh digunakan oleh nim, jadi kami terpaksa menulisnya sendiri.

use minify_html::{Cfg, minify as minify_html_native};
use std::ffi::CStr;
use std::ffi::CString;
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn minify(
    code: *const c_char,
    do_not_minify_doctype: bool,
    ensure_spec_compliant_unquoted_attribute_values: bool,
    keep_closing_tags: bool,
    keep_comments: bool,
    keep_html_and_head_opening_tags: bool,
    keep_spaces_between_attributes: bool,
    minify_css: bool,
    minify_js: bool,
    remove_bangs: bool,
    remove_processing_instructions: bool,
) -> *const c_char {

    let code = unsafe { CStr::from_ptr(code) };
    let code_vec = code.to_bytes();

    let cfg = Cfg {
        do_not_minify_doctype,
        ensure_spec_compliant_unquoted_attribute_values,
        keep_closing_tags,
        keep_comments,
        keep_html_and_head_opening_tags,
        keep_spaces_between_attributes,
        minify_css,
        minify_js,
        remove_bangs,
        remove_processing_instructions,
    };

    let minified = minify_html_native(code_vec, &cfg);

    let s = unsafe { CString::from_vec_unchecked(minified).into_raw() };
    return s;
}

Nimpy dan usaha untuk pemadaman sampah bebas kemalangan

Pengikatan Python untuk nim perlu ofc percuma membuang objek python. Masalahnya kita kena kawal bila nim buat GC. Perpustakaan nimpy menganggap GIL sentiasa dikunci (ia menguncinya pada mulanya), dan ia adalah percuma untuk memanggil ular sawa bila-bila masa. Tetapi kami membuka kunci gil untuk membolehkan kumpulan benang ular sawa menjalankan kod semasa nim menjalankan perkara lain. Jika GIL ular sawa sentiasa dikunci oleh nim threadpool akan melahu kebanyakan masa.

when defined(pyAsync):
  type
    PyGilObj = object
      lock: ThreadLock
      currentLockHolder: int
      state: PyGILState_STATE
    PyGil = ptr PyGilObj

  var pyGil*: PyGil
  var pyGilLock*: ThreadLock
  var pyMainThread: PyThreadState
  proc initPyGil*() =
    assert PyGILState_Check()
    pyGil = create(PyGilObj)
    pyGil.currentLockHolder = getThreadID()
    pyGil.lock = newThreadLock()
    pyGilLock = pyGil.lock
    pyMainThread = PyEval_SaveThread()

  proc acquire*(gil: PyGil): Future[void] {.async.} =
    await gil.lock.acquire
    let id = getThreadId()
    gil.currentLockHolder = id
    gil.state = Py_GILState_Ensure()

  proc tryAcquire*(gil: PyGil): bool =
    if gil.lock.tryAcquire():
      let id = getThreadId()
      gil.currentLockHolder = id
      gil.state = Py_GILState_Ensure()
      return true

  proc release*(gil: PyGil) {.inline.} =
    doassert gil.currentLockHolder == getThreadId(), "Can't release gil lock from a different thread."
    doassert gilLocked()
    Py_GILState_Release(gil.state)
    gil.lock.release

Ini membolehkan cara untuk melaksanakan kod python yang memegang GIL, tetapi hanya pada benang semasa. Pelaksanaan untuk memperoleh/melepaskan GIL pada benang nim yang berbeza memerlukan memanggil fungsi python C abi yang berbeza, kerana GIL ialah mutex. Kami kemudian memanggil python menggunakan templat ini:

template withPyLock*(code): untyped =
  {.locks: [pyGil].}:
    try:
      # echo getThreadId(), " -- ", getCurrentProcessId(), " -- ", procName()
      await pygil.acquire()
      code
    except:
      raise getCurrentException()
    finally:
      # echo getThreadId(), " -- ", getCurrentProcessId(),  " -- unlocked"
      pygil.release()

Kami menggunakan ciri kunci dan pengawal nim, untuk memastikan jenis ular sawa hanya diakses apabila GIL diadakan. Walau bagaimanapun ini memerlukan penentuan pyobjek dengan pengawal:

macro pyObjPtr*(defs: varargs[untyped]): untyped =
  result = newNimNode(nnkStmtList)
  for d in defs:
    let
      name = d[0]
      def = d[1]
    result.add quote do:
      let `name` {.guard: pyGil.} = create(PyObject)
      `name`[] = `def`

Jadi saya boleh lakukan:

pyObjPtr(myVar, pyimport("datetime").datetime))

Dan setiap kali saya menelefonmyVar yang memegang objek datetime, saya perlu membungkusnya seperti ini:

withPyLock():
  myVar.fromunixtimestamp(1)

Sekarang kita boleh mengunci gil apabila kita perlu menjalankan GC, mengatasi lincahPyObject pemusnah dengan ini:

var garbage: seq[PPyObject]

proc `=destroy`*(p: var PyObject) =
  if pygil.tryAcquire:
    if not p.rawPyObj.isnil:
      decRef p.rawPyObj
      p.rawPyObj = nil
    while garbage.len > 1:
      var pp = garbage.pop() # TODO: Does this leak a pointer?
      if not pp.isnil:
        decRef pp
      pp = nil
    pygil.release
  else:
    if not p.rawPyObj.isnil:
      garbage.add p.rawPyObj

Kunci yang kami gunakan di dalam pemusnah bukanlahAsyncLock kerana itu akan menjadi terlalu mahal, dan kami jangan sentiasa kunci, kerana itu akan menyebabkan gerai! Jika kami tidak dapat mengunci gil, kami menangguhkan pengumpulan dan menyimpan penuding ular sawa mentah di sekeliling untuk bila kami dapat mengosongkannya. Sejujurnya saya tidak tahu sama ada ini menyebabkan bentuk isu lain, tetapi nampaknya berkesan cukup baik.

Kami mempunyai modul nim yang dipanggilpyutils.nim yang melakukan sekumpulan nim<>barangan python, sebagai contoh:

from utils import withLocks
proc pyhasAttr*(o: PyObject; a: string): bool {.withLocks: [pyGil].} = pybi[].hasattr(
    o, a).to(bool)

proc pyclass(py: PyObject): PyObject {.inline, withLocks: [pyGil].} =
  pybi[].type(py)

proc pytype*(py: PyObject): string =
  py.pyclass.getattr("__name__").to(string)

proc pyisbool*(py: PyObject): bool {.withLocks: [pyGil].} =
  return pybi[].isinstance(py, PyBoolClass[]).to(bool)

proc pyisnone*(py: PyObject): bool {.gcsafe, withLocks: [pyGil].} =
  return py.isnil or pybi[].isinstance(py, PyNoneClass[]).to(bool)

Yang ini digunakan agak banyak:

proc pyget*[T](py: PyObject; k: string; def: T = ""): T =
  try:
    let v = py.callMethod("get", k)
    if pyisnone(v):
      return def
    else:
      return v.to(T)
  except:
    pyErrClear()
    if pyisnone(py):
      return def
    else:
      return py.to(T)

Yang ini digunakan apabila kami telah menjadualkan kerja ular sawa, dan kami mahu menunggu sehingga selesai secara tidak segerak:

proc pywait*(j: PyObject): Future[PyObject] {.async, gcsafe.} =
  var rdy: bool
  var res: PyObject
  while true:
    withPyLock:
      checkNil(j)
      rdy = j.callMethod("ready").to(bool)
    if rdy:
      withPyLock:
        checkNil(j)
        res = j.callMethod("get")
      break
    await sleepAsync(250.milliseconds)
  withPyLock:
    if (not res.isnil) and (not pyisnone(res)) and (not pyErrOccurred()):
      return res
    else:
      raise newException(ValueError, "Python job failed.")

Pengikatan async python yang betul akan memerlukan melengkapkan masa depan nim async daripada python pada penghujung kerja berjadual ular sawa, yang tidak kami lakukan kerana kami tidak melihat cukup mendalam untuk mengendalikan objek nim daripada python.

Penguatan

Kami menyokong google amp, jadi kami menjana halaman amp yang agak mematuhi amp. Kami tidak menyasarkan sokongan 1:1. Sebenarnya kami nuke semua javascript yang kami ada dan hanya menyajikan html/css. Walaupun begitu kita perlu berhati-hati untuk tidak menambah atribut tersuai pada teg html, atau hanya teg html tersuai, amp adalah buruk seperti itu... Untuk penukaran halaman amp automatik kami mengendalikanhead dan jugabody tag secara berbeza.

proc processHead(inHead: VNode, outHead: VNode, level = 0) {.async.} =
  var canonicalUnset = level == 0
  debug "iterating over {inHead.kind}"
  for el in inHead.preorder(withStyles = true):
    case el.kind:
      of VNodeKind.text, skipNodes:
        continue
      of VNodeKind.style:
        if el.len > 0:
          el[0].text.maybeStyle
      of VNodeKind.link:
        if canonicalUnset and el.isLink(canonical):
          outHead.add el
          canonicalUnset = false
        elif el.isLink(stylesheet) and (not ("flags-sprite" in el.getattr("href"))):
          await el.fetchStyle()
        elif el.isLink(preload) and el.getattr("as") == "style":
          await el.fetchStyle()
        else:
          outHead.add el
      of VNodeKind.script:
        if el.getAttr("type") == $ldjson:
          outHead.add el
      of VNodeKind.meta:
        if (el.getAttr("name") == "viewport") or (el.getAttr("charset") != ""):
          continue
        else:
          outHead.add el
      of VNodeKind.verbatim:
        let data = el.toXmlNode
        if data.kind == xnElement:
          if data.tag == "noscript":
            processNoScript()
          elif data.tag == "script":
            continue
          elif data.tag == "style":
            if data.len > 0:
              data[0].text.maybeStyle
          else:
            outHead.add el
      of VNodekind.noscript:
        processNoScript()
      else:
        debug "amphead: adding element {el.kind} to outHead."
        outHead.add el

Semua gaya digabungkan menjadi skrip sebaris tunggal, apa yang disimpan ialahlink teg yang bukan gaya/jskrip, seperti lang. Tag skrip untukldljson, meta tag. Verbatim mengendalikan nod yang literal , kita perlu menukarkannya kepadaXmlNode (yang bermaksud menghuraikan) dan mengendalikannya dengan betul. Badan proses adalah serupa, kami menyimpan beberapa teg, mengalih keluar yang lain, menamakan yang lain:

template process(el: VNode, after: untyped): bool =
  var isprocessed = true
  case el.kind:
    of skipNodes: discard
    of VNodeKind.link:
      if el.isLink(stylesheet):
        await el.fetchStyle()
      else:
        outBody.add el
    of VNodeKind.style:
      el.text.maybeStyle
      el.text = ""
    of VNodeKind.script:
      if el.getAttr("type") == $ldjson:
        outHead.add el
      el.text = ""
    of VNodeKind.form:
      el.setAttr("amp-form", "")
    else:
      isprocessed = false
  if isprocessed:
    after
  isprocessed

Theform tag digantikan denganamp-form , amp mempunyai banyak teg ini...

Kami perlu memastikan bahawa gaya sebaris berada dalam panjang yang betul:

styleStr = styleStr
  # .join("\n")
  # NOTE: the replacement should be ordered from most frequent to rarest
  # # remove troublesome animations
  .replace(pre"""\s*[email protected](\-[a-zA-Z]+-)?keyframes\s+?.+?{\s*?.+?({.+?})+?\s*?}""", "")
  # # remove !important hints
  .replace(pre"""!important""", "")
  # remove charset since not allowed
  .replace(pre"""@charset\s+\"utf-8\"\s*;?/i""", "")

if unlikely(styleStr.len > CSS_MAX_SIZE):
  raise newException(ValueError, fmt"Style size above limit for amp pages. {styleStr.len}")

Penjanaan amp kami tidak meliputi spesifikasi amp penuh, tetapi ia berfungsi untuk kandungan kami (melalui percubaan dan ralat :S).

Setiap kali artikel diterbitkan, ia diserap ke dalam pangkalan data sonik, pangkalan data sonik mengendalikan "koleksi", "baldi" dan "objek"; Kami mentakrifkan koleksi sebagai tapak web, jadi setiap tapak web yang ingin menggunakan pengagregat kandungan mempunyai koleksinya sendiri. Kami tidak menggunakanbuckets , walaupun kami boleh menganggap setiap topik sebagai baldi yang akan menyempitkan carian terlalu banyak, jadi setiap tapak hanya mempunyai satu baldi "lalai", dan setiap objek baldi ialah artikel (yang boleh terdiri daripada topik yang berbeza).

proc push*(capts: UriCaptures, content: string) {.async.} =
  ## Push the contents of an article page to the search database
  ## NOTE: NOT thread safe
  var ofs = 0
  while ofs <= content.len:
    let view = content[ofs..^1]
    let key = join([capts.topic, capts.page, capts.art], "/")
    let cnt = runeSubStr(view, 0, min(view.len, bufsize - key.len))
    ofs += cnt.len
    if cnt.len == 0:
      break
    try:
      let lang = await capts.lang.toISO3
      var pushed: bool
      var j: PyObject
      withPyLock:
        j = pySched[].apply(
          pySonic[].push,
          WEBSITE_DOMAIN,
          "default", # TODO: Should we restrict search to `capts.topic`?
          key,
          cnt,
          lang = if capts.lang != "en": lang else: ""
          )
      j = await j.pywait()
      withPyLock:
        pushed = not pyisnone(j) and j.to(bool)
      when not defined(release):
        if not pushed:
          capts.addToBackLog()
          break
    except Exception:
      logexc()
      debug "sonic: couldn't push content, \n {capts} \n {key} \n {cnt}"
      when not defined(release):
        capts.addToBackLog()
        block:
          var f: File
          try:
            await pushLock[].acquire
            f = open("/tmp/sonic_debug.log", fmWrite)
            write(f, cnt)
          finally:
            pushLock[].release
            if not f.isnil:
              f.close()
      break

Apabila menolak kandungan ke dalam sonik kita perlu membahagikan data dalam ketulan, yang panjang maksimum diketahui semasa sambungan. Memasukkan data nampaknya kadang-kadang bermasalah, kerana ia nampaknya tidak dapat mengendalikan beberapa aksara tertentu. Sekiranya pelayan sonik rosak entah bagaimana, kami juga mempunyai fungsi untuk menyerap semula semua kandungan:

proc pushAllSonic*() {.async.} =
  await syncTopics()
  var total, c, pagenum: int
  let pushLog = await readPushLog()
  if pushLog.len == 0:
    withPyLock:
      discard pySonic[].flush(WEBSITE_DOMAIN)
  defer:
    withPyLock:
      discard pySonic[].consolidate()
  for (topic, state) in topicsCache:
    if topic notin pushLog:
      pushLog[topic] = %0
    await pygil.acquire
    defer: pygil.release
    let done = state.group[]["done"]
    for page in done:
      pagenum = ($page).parseint
      c = len(done[page])
      if pushLog[topic].to(int) >= pagenum:
        continue
      var futs: seq[Future[void]]
      for n in 0..<c:
        let ar = done[page][n]
        if ar.isValidArticlePy:
          var relpath = getArticlePath(ar, topic)
          relpath.removeSuffix("/")
          let
            capts = uriTuple(relpath)
            content = ar.pyget("content").sanitize
          echo "pushing ", relpath
          futs.add push(capts, content)
          total.inc
      pygil.release
      await allFutures(futs)
      pushLog[topic] = %pagenum
      await writePushLog(pushLog)
      await pygil.acquire
  info "Indexed search for {WEBSITE_DOMAIN} with {total} objects."

Terjemahan

Terjemahan adalah cerita yang agak berantakan. Saya berada di peringkat ke-4 (!) pelaksanaan pembalut terjemahan, selepas menulis dalam php, pergi dan julia , ini juga ditulis dalam nim. Varian php/go agak busuk pada masa kini, manakala varian julia aktif digunakan untuk blog ini. Walau bagaimanapun untuk mencapai kelewatan yang rendah untuk pelayan web, cara penterjemahan dilaksanakan dalam julia tidak sesuai untuk servis masa nyata (ia menterjemah fail statik), dan bagaimanapun, menambah julia sebagai pergantungan apabila kita sudah mempunyai python akan menjadi sangat besar. keperluan.

Jadi saya terpaksa melaksanakan modul terjemahan baharu dalam nim. Sebenarnya, modul terjemahan nim awal kelihatan seperti pelaksanaan julia, di mana kami menterjemah fail statik[1] . Selepas itu, apabila pelayan web mula dibentuk, saya menukarnya untuk menterjemah nod karax atas permintaan. Ini membolehkan untuk menterjemah setiap halaman web tepat pada masanya untuk permintaan.

template translateVbtm(node: VNode, q: QueueDom) =
  assert node.kind == VNodeKind.verbatim
  let tree = ($node).parseHtml() # FIXME: this should be a conversion, but the conversion doesn't preserve whitespace??
  if tree.kind == xnElement and tree.tag == "document":
    tree.tag = "div"
  takeOverFields(tree.toVNode, node)
  translateIter(node, vbtm = false)

template translateIter(otree; vbtm: static[bool] = true) =
  for el in otree.preorder():
    case el.kind:
      of vdom.VNodeKind.text:
        if el.text.isEmptyOrWhitespace:
          continue
        if isTranslatable(el):
          translate(q.addr, el, srv)
      else:
        let t = el.kind
        if t in tformsTags:
          getTForms(dom)[t](el, file_path, url_path, pair)
        if t == VNodeKind.a:
          if el.hasAttr("href"):
            rewriteUrl(el, rewrite_path, hostname)
        if t == VNodeKind.verbatim:
          when vbtm:
            debug "dom: translating verbatim", false
            translateVbtm(el, q)
        else:
          if(el.hasAttr("alt") and el.isTranslatable("alt")) or
            (el.hasAttr("title") and el.isTranslatable("title")):
            translate(q.addr, el, srv)

Di atas ialah gelung lelaran utamatranslateIter:

Terjemahan digunakan pada semua nod teks dan padaalt dantitle sifat-sifat.

proc translate*[T](q: ptr[QueueXml | QueueDom], el: T, srv: service) =
  if q.isnil:
    warn "translate: queue can't be nil"
    return
  let (success, length) = setFromDB(q[].pair, el)
  if not success:
    if length > q[].bufsize:
      debug "Translating element singularly since it is big"
      elUpdate(q[], el, srv)
    else:
      if reachedBufSize(length, q[]):
        q[].push()
      q[].bucket.add(el)
      q[].sz += length

proc translate*[T](q: ptr[QueueXml | QueueDom], el: T, srv: service,
    finish: bool): Future[bool] {.async.} =
  if finish:
    if q.isnil:
      return true
    let (success, _) = setFromDB(q[].pair, el)
    if not success:
      addJob(@[el], q[], el.getText)
      debug "translate: waiting for pair: {q[].pair}"
      await doTrans()
  return true

proc translate*(q: ptr[QueueXml | QueueDom], srv: service,
    finish: bool): Future[bool] {.async.} =
  if finish and q[].sz > 0:
    q[].push()
    await doTrans()
    saveToDB(force = true)
  return true

Kerana kita perlu menterjemah setiap nod teks secara berasingan (jika tidak, kita tidak boleh mengembalikan html) setiap terjemahan nod adalah tugas yang berasingan. Memandangkan kerja boleh menanyakan perkhidmatan terjemahan jaring, ia perlu dilakukan secara tidak segerak. Kami melakukan pemisahan dan penggabungan pertanyaan terjemahan untuk menggantikan panggilan api, tetapi bahagian dalaman enjin terjemahan tidak penting untuk diketahui. Satu-satunya perkara yang perlu diperhatikan ialah pada mulanya saya menggunakan a pembalut ular sawa (yang masih saya gunakan untuk menterjemah kandungan yang dikikis) kerana mengurus sendiri pembalut untuk apis luaran adalah menyakitkan, tetapi kemudian beralih kepada perkhidmatan terjemahan google dan yandex yang dibalut sendiri dalam nim, kerana python menjadi halangan yang besar apabila mengendalikan banyak terjemahan serentak.

[1]Sebenarnya pada asalnya agregator kandungan sepatutnya hanya menjana fail statik untukcaddy untuk disiarkan, tetapi kerana jumlah halaman untuk dijana (iaitu matriks n_lang(20) x amp(2) x halaman), pemaparan malas ialah pilihan yang lebih baik.

Statistik

Halaman topik dan artikel dijejaki untuk jumlah hit.

proc updateHits*(capts: UriCaptures) =
  let ak = join([capts.topic, capts.art])
  let tk = capts.topic
  var
    artCount: int32 = statsDB[ak]
    topicCount: int32 = statsDB[tk]
  artCount += 1
  topicCount += 1
  statsDB[ak] = artCount
  statsDB[tk] = topicCount

Kami menggunakan kiraan hit untuk membersihkan halaman dengan kiraan rendah secara berkala.

proc deleteLowTrafficArts*(topic: string): Future[void] {.gcsafe, async.} =
  let now = getTime()
  var
    pagenum: int
    pagesToReset: seq[int]
    pubTime: Time
    pubTimeTs: int
  var capts = mUriCaptures()
  capts.topic = topic
  for (art, _) in (await publishedArticles[string](topic, "")):
    withPyLock:
      if pyisnone(art):
        continue
      capts.art = pyget[string](art, "slug")
      pagenum = pyget(art, "page", 0)
    capts.page = pagenum.intToStr
    try:
      withPyLock:
        pubTimeTs = pyget(art, "pubTime", 0)
      pubTime = fromUnix(pubTimeTs)
    except:
      pubTime = default(Time)
    if pubTime == default(Time):
      if not (pagenum in pagesToReset):
        debug "tasks: resetting pubTime for page {pagenum}"
        pagesToReset.add pagenum
    # article is old enough
    elif inSeconds(now - pubTime) > cfg.CLEANUP_AGE:
      let hits = topic.getHits(capts.art)
      # article has low hit count
      if hits < cfg.CLEANUP_HITS:
        await deleteArt(capts)
  for n in pagesToReset:
    withPyLock:
      discard site[].update_pubtime(topic, n)

Pangkalan data

Kami gunalibmdbx melalui lib ini . Mungkin berlebihan, dan menggunakan leveldb sudah memadai. Kami mempunyai jenisLRUTrans di mana idea awal adalah untuk menyediakan pangkalan data sebagai cache LRU, tetapi ia jauh lebih perlahan. Pelaksanaannya boleh didapati di sini

type
    CollectionNotNil = ptr Collection not nil
    LRUTransObj = object
        db: nimdbx.Database.Database not nil
        coll: CollectionNotNil
        zstd_c: ptr ZSTD_CCtx
        zstd_d: ptr ZSTD_DCtx
    LRUTrans* = ptr LRUTransObj

proc getImpl(t: LRUTrans, k: int64, throw: static bool): string =
    withLock(tLock):
        var o: seq[byte]
        t.coll.inSnapshot do (cs: CollectionSnapshot):
            # debug "nimdbx: looking for key {k}, {v}"
            o.add cs[k.asData].asByteSeq
        if len(o) > 0:
            result = cast[string](decompress(t.zstd_d, o))
            # debug "nimdbx: got key {k}, with {o.len} bytes"
        elif throw:
            raise newException(KeyError, "nimdbx: key not found")

proc getImpl[T: not int64](t: LRUTrans, k: T, throw: static bool): string =
    getImpl(t, hash(k).int64, throw)


proc `[]`*[T](t: LRUTrans, k: T): auto = t.getImpl(k, false)
proc `get`*[K](t: LRUTrans, k: K): auto = t.getImpl(k, true)

proc `[]=`*(t: LRUTrans, k: int64, v: string) {.gcsafe.} =
    var o: seq[byte]
    if likely(v.len != 0):
      o = compress(t.zstd_c, v, cfg.ZSTD_COMPRESSION_LEVEL)
    withLock(tLock):
        logall "nimdbx: saving key {k}"
        t.coll.inTransaction do (ct: CollectionTransaction):
            {.cast(gcsafe).}:
                ct[k] = o
            ct.commit()
        logall "nimdbx: commited key {k}"

proc `[]=`*[K: not int64](t: LRUTrans, k: K, v: string) = t[hash(k).int64] = v

Jenis ini digunakan untuk empat pangkalan data berasingan:

Jenis pangkalan data dilaksanakan dengan getter dan setter kemudian lakukan penyahmampatan automatik pada baca/tulis. Atas sebab ini ia tidak sepatutnya digunakan untuk imej...tetapi malangnya... Terdapat juga sekumpulan cache mikro kecil:

Ini dilaksanakan sebagai lru cache[2] , lebih tepat sebagai cache lru "terkunci", di mana setiap operasi perolehan dan set dililitkan pada kunci (benang). Kunci ini tidak boleh menyebabkan gerai dengan masa jalan tak segerak kerana kunci diperoleh dan dilepaskan tanpa sebarang pernyataan hasil, jadi kunci itu adalah atom dalam erti kata itu, walau bagaimanapun ia masih berguna kerana kami menggunakan benang untuk tugasan yang berbeza.

[2]Namun begitu nim rebus mempunyai pelaksanaan yang lebih mudah untuk cache lru yang saya akan gunakan jika ditemui lebih awal.

Pekerjaan Latar Belakang

Beberapa tugasan yang kami gunakan adalah CPU lapar, jadi kami menggunakan urutan yang berbeza untuk mereka:

Dua lagi urutan digunakan untuk mengemas kini senarai fail aset dan iklan, walaupun tidak cpu lapar, benang diperlukan untuk mengelakkan gerai yang disebabkan oleh pemerhati fail.

Kami juga mempunyai tugasan berjalan lama async untuk:

Lsh, imej, terjemahan dan kerja permintaan http dikendalikan menggunakan persediaan pengeluar/pengguna. Kecuali kami tidak menggunakan saluran, kerana saluran menyekat dan kami tidak mempunyai pelaksanaan async untuknya yang juga selamat untuk benang. Kami menggunakan pelaksanaan async bagi ini[1] . Dan jadual async, yang seperti bas acara

type
  AsyncTableObj[K, V] = object
    lock: ThreadLock
    waiters: Table[K, seq[ptr Future[V]]]
    table: Table[K, V]
  AsyncTable*[K, V] = ptr AsyncTableObj[K, V]

proc pop*[K, V](t: AsyncTable[K, V], k: K): Future[V] {.async.} =
  var popped = false
  withLock(t.lock):
    if k in t.table:
      popped = t.table.pop(k, result)
  if not popped:
    if k notin t.waiters:
      t.waiters[k] = newSeq[ptr Future[V]]()
    var fut = newFuture[V]("AsyncTable.pop")
    t.waiters[k].add fut.addr
    result = await fut

proc put*[K, V](t: AsyncTable[K, V], k: K, v: V) {.async.} =
  withLock(t.lock):
    if k in t.waiters:
      var ws: seq[ptr Future[V]]
      doassert t.waiters.pop(k, ws)
      while ws.len > 0:
        let w = ws.pop()
        if not w.isnil and not w[].isnil and not w[].finished:
          w[].complete(v)
    else:
      t.table[k] = v

Pelayan nim juga mengendalikan tiga tugas async:

type
  TaskKind = enum pub, cleanup, mem

proc scheduleTasks(): TaskTable =
  template addTask(t) =
    let fut = (selectTask t)()
    result[t] = fut
  # Publishes new articles for one topic every x seconds
  addTask pub
  # cleanup task for deleting low traffic articles
  addTask cleanup
  # quit when max memory usage reached
  addTask mem

Tugas yang memantau penggunaan mem adalah bagus untuk dilakukan, untuk mengelakkan masalah OOM antara proses kontena dan docker, kerana docker (atau kernel) tidak mematikan proses serta-merta, dan dalam tempoh masa ini pelayan boleh menjadi tidak bertindak balas, jadi adalah lebih baik untuk memulakan semula secara manual dengan segera.

[1]walaupun membungkus saluran biasa dalam rutin async mungkin lebih baik...sayangnya

Imej

Kami memanfaatkan aliran imej untuk mengubah saiz dan cache imej secara setempat. Pengikatannya mudah, tetapi prosesnya sedikit terlibat. DengangetImg kami mengambil data imej dari url jauh:

proc getImg*(src: string, kind: Source): Future[string] {.async.} =
  return case kind:
    of urlsrc:
      (await get(src.parseUri, decode = false, proxied = false)).body
    elif fileExists(src):
      await readFileAsync(src)
    else:
      ""

Kemudian kita perlu menambahkannya ke konteks aliran imej:

proc addImg*(img: string): bool =
  ## a lock should be held here throughout the `processImg` call.
  if img == "": return false
  reset(ctx)
  doassert ctx.check
  let a = imageflow_context_add_input_buffer(
    ctx.p,
    inputIoId,
    # NOTE: The image is held in cache, but it might be collected
    cast[ptr uint8](img[0].unsafeAddr),
    img.len.csize_t,
    imageflow_lifetime_lifetime_outlives_context)
  if not a:
    doassert ctx.check
    cmdStr["decode"] = %inputIoId
  return true

Jika imej tidak boleh ditambah, ini bermakna aliran imej gagal mengenali data sebagai imej yang sah. Selepas kami menghantar data, kami perlu menghantar pertanyaan kepada konteks, kemudian membaca respons, dan mendapatkan output:

proc doProcessImg(input: string, mtd = execMethod): (string, string) =
  setCmd(input)
  let c = $cmd
  # debug "{hash(c)} - {c}"
  let json_res = imageflow_context_send_json(
      ctx.p,
      mtd,
      cast[ptr uint8](c[0].unsafeAddr),
      c.len.csize_t
    )
  discard imageflow_json_response_read(ctx.p, json_res,
                                       status.addr,
                                       resPtr,
                                       resLen)
  defer: doassert imageflow_json_response_destroy(ctx.p, json_res)

  var mime: string
  if status != 200:
    let msg = resPtr[].toString(resLen[].int)
    debug "imageflow: conversion failed {msg}"
    doassert ctx.check
  else:
    mime = getMime()
  discard imageflow_context_get_output_buffer_by_id(
      ctx.p,
      outputIoId,
      outputBuffer,
      outputBufferLen)
  doassert ctx.check
  result = (outputBuffer[].toString(outputBufferLen[].int), mime)

Kami mendapat jenis mime daripada respons, yang akan dimajukan dalam respons pelayan web. Dari sisi pelayan terjemahan dari laluan url ke aliran imej dikendalikan seperti ini:

proc processImgData(q: ptr ImgQuery) {.async.} =
  # push img to imageflow context
  initImageFlow() # NOTE: this initializes thread vars
  var acquired, submitted: bool
  let data = (await q.url.rawImg)
  defer:
    if acquired: imgLock[].release
    if not submitted:
      imgOut[q] = true
  if data.len > 0:
    try:
      await imgLock[].acquire
      acquired = true
      if addImg(data):
        let query = fmt"width={q.width}&height={q.height}&mode=max&format=webp"
        logall "ifl server: serving image hash: {hash(await q.url.rawImg)}, size: {q.width}x{q.height}"
        # process and send back
        (q.processed.data, q.processed.mime) = processImg(query)
        imgOut[q] = true
        submitted = true
    except Exception:
      discard

Url imej dihantar sebagai parameter, dalam bentuk termampat zstd. Pemampatan memendekkan url (kebanyakan masa). Ini juga cara saya menemui pepijat dalam google chrome, di mana ia tidak dapat mengendalikan url yang pertanyaannya mempunyai data termampat yang dikodkan url. Firefox baik-baik saja.

LD-JSON

Kami menambah pada setiap halaman web skrip ldjson .

proc jwebpage(id, title, url, mtime, selector, description: auto, keywords: seq[string], name = "", headline = "",
            image = "", entity = "Article", status = "Published", lang = "english", mentions: seq[
            string] = (@[]), access_mode = (@["textual", "visual"]), access_sufficient: seq[
            string] = @[], access_summary = "", created = "", published = "",
            props = default(JsonNode)): JsonNode =
    let
        d_mtime = coerce(mtime, "")
        s_created = created.toIsoDate
        description = coerce(description, to = title)
        prd = (v: seq[string]) => v.len == 0

    let data = %*{
        "@context": "https://schema.org",
        "@type": "https://schema.org/WebPage",
        "@id": id,
        "url": url,
        "lastReviewed": coerce(mtime, ""),
        "mainEntityOfPage": {
            "@type": entity,
            "@id": url
        },
        "mainContentOfPage":
        {
            "@type": "WebPageElement", "cssSelector": selector},
        "accessMode": access_mode,
        "accessModeSufficient": {
            "@type": "itemList",
            "itemListElement": coercf(access_sufficient, prd, to = access_mode),
        },
        "creativeWorkStatus": status,
        # NOTE: datePublished should always be provided
        "datePublished": ensure_time(d_mtime.toIsoDate, s_created),
        "dateModified": d_mtime,
        "dateCreated": coerce(s_created, to = d_mtime),
        "name": coerce(name, to = title),
        "description": coerce(description, ""),
        "keywords": coerce(keywords, to = (@[]))
    }
    setArgs data, %*{"inLanguage": lang, "accessibilitySummary": access_summary,
                    "headline": coerce(headline, to = description), "image": image,
                    "mentions": mentions}
    setProps
    data

Dan untuk halaman terjemahan:

proc translation*(src_url, trg_url, lang, title, mtime, selector, description: auto, keywords: seq[string],
                     image = "", headline = "", props = default(JsonNode),
                     translator_name = "Google", translator_url = "https://translate.google.com/"): auto =
    ## file path must be relative to the project directory, assumes the published website is under '__site/'
    # id, title, url, mtime, selector, description: auto, keywords: seq[string], name = "", headline = "",
    let data = jwebpage(id = trg_url, title, url = trg_url, mtime, selector, description,
                            keywords = keywords, image = image, headline = headline, lang = lang, props = props)
    data["translator"] = %*{"@type": "https://schema.org/Organization",
                             "name": translator_name,
                             "url": translator_url}
    data["translationOfWork"] = %*{"@id": src_url}
    data

Opengraph

Sama seperti ldjson, kami juga menyediakan tag meta opengraph:

proc opgBasic(title, tp, url, image: string, prefix = ""): seq[XmlNode] =
  if prefix != "":
    result.add metaTag(fmt"{prefix}:title", title)
    result.add metaTag(fmt"{prefix}:type", tp)
    result.add metaTag(fmt"{prefix}:url", url)
    result.add metaTag(fmt"{prefix}:image", image)
  else:
    result.add metaTag("title", image)
    result.add metaTag("type", image)
    result.add metaTag("url", image)
    result.add metaTag("image", image)

proc opgTags(title, tp, url,
             image: string,
             description = "",
             siteName = "",
             locale = "",
             audio = "",
             video = "",
             determiner = "",
             prefix = ""): seq[XmlNode] {.gcsafe.} =
  ## Generates an HTML String containing opengraph meta result for one item.
  var result = opgBasic(title, tp, url, image, prefix)
  result.add opgOptional(description, siteName, locale, audio, video, determiner)
  return result

proc opgPage*(a: Article): seq[XmlNode] =
  let locale = static(DEFAULT_LOCALE)
  let
    tp = static("article")
    url = getArticleUrl(a)
    siteName = static(WEBSITE_TITLE)
  result = opgTags(a.title, tp, url, a.imageUrl, a.desc, siteName, locale, prefix = "article")
  for t in a.tags:
    result.add metaTag("article:tag", t)
  result.add metaTag("article:author", a.author)
  result.add metaTag("article:published_time", $a.pubTime)
  result.add metaTag("article:section", a.desc)
  # result.add metaTag("article:modified_time", a.pubTime)
  # result.add metaTag("article:expiration_time", a.pubTime)
  result.add twitterMeta("card", "summary")
  result.add twitterMeta("creator", twitterUrl[])

Makro dan templat nim berguna apabila berurusan dengan semua kod berat boilerplate ini.

Permintaan http sebelah pelayan

Terdapat satu lagi tugas, yang mengendalikan semua permintaan http (untuk mengambil imej, skrip, dll) dari sisi pelayan web. Kami menggunakan chronos httpclient:

const proxiedFlags = {NoVerifyHost, NoVerifyServerName, NewConnectionAlways}
const sessionFlags = {NoVerifyHost, NoVerifyServerName, NoInet6Resolution}
proc requestTask(q: sink ptr Request) {.async.} =
  var trial = 0
  var
    sess: HttpSessionRef
    req: HttpClientRequestRef
    resp: HttpClientResponseRef
    cleanup: seq[Future[void]]
  while trial < q[].retries:
    try:
      trial.inc
      sess = new(HttpSessionRef,
                proxyTimeout = 10.seconds.div(3),
                headersTimeout = 10.seconds.div(2),
                connectTimeout = 10.seconds,
                proxy = if q[].proxied: selectProxy(trial) else: "",
                flags = if q[].proxied: proxiedFlags else: sessionFlags
      )
      req = new(HttpClientRequestRef,
                sess,
                sess.getAddress(q[].url).get,
                q[].meth,
                headers = q[].headers.toHeaderTuple,
                body = q[].body.tobytes,
        )
      resp = await req.fetch(followRedirects = q[].redir, raw = true)
      checkNil(resp):
        defer:
          cleanup.add resp.closeWait()
          resp = nil
        q.response.code = httpcore.HttpCode(resp.status)
        checkNil(resp.connection):
          q.response.body = bytesToString (await resp.getBodyBytes)
          q.response.headers = newHttpHeaders(cast[seq[(string, string)]](
              resp.headers.toList))
        break
    except:
      cdebug():
        logexc()
        debug "cronhttp: request failed"
    finally:
      if not req.isnil:
        cleanup.add req.closeWait()
      if not resp.isnil:
        cleanup.add resp.closeWait()
      if not sess.isnil:
        cleanup.add sess.closeWait()
  httpOut[q] = true
  await allFutures(cleanup)

Saya terpaksa menambah sokongan untuk proksi https dan socks5 kepada httpclient untuk dapat menggunakan terjemahan dengan berkesan.

Konfigurasi

Anda mungkin perasan pembolehubah huruf besar sepanjang kod. Semua ini adalah pembolehubah konfigurasi, yang ditakrifkan dalam fail, yang boleh disesuaikan setiap tapak web.

const
  BASE_URL* = Uri()
  SITE_PATH* = PROJECT_PATH / "site"
  SITE_ASSETS_PATH* = BASE_URL / "assets" / WEBSITE_NAME
  SITE_ASSETS_DIR* = SITE_PATH / "assets" / WEBSITE_NAME
  DATA_PATH* = PROJECT_PATH / "data"
  DATA_ASSETS_PATH* = DATA_PATH / "assets" / WEBSITE_NAME
  DATA_ADS_PATH* = DATA_PATH / "ads" / WEBSITE_NAME
  ASSETS_PATH* = PROJECT_PATH / "src" / "assets"
  DEFAULT_IMAGE* = ASSETS_PATH / "empty.png"
  DEFAULT_IMAGE_MIME* = "image/png"
  CSS_BUN_URL* = $(SITE_ASSETS_PATH / "bundle.css")
  CSS_CRIT_PATH* = SITE_ASSETS_DIR / "bundle-crit.css"
  JS_REL_URL* = $(SITE_ASSETS_PATH / "bundle.js")
  LOGO_PATH* = BASE_URL / "assets" / "logo" / WEBSITE_NAME
  LOGO_URL* = $(LOGO_PATH / "logo.svg")
  LOGO_SMALL_URL* = $(LOGO_PATH / "logo-small.svg")
  LOGO_ICON_URL* = $(LOGO_PATH / "logo-icon.svg")
  LOGO_DARK_URL* = $(LOGO_PATH / "logo-dark.svg")
  LOGO_DARK_SMALL_URL* = $(LOGO_PATH / "logo-small-dark.svg")
  LOGO_DARK_ICON_URL* = $(LOGO_PATH / "logo-icon-dark.svg")
  FAVICON_PNG_URL* = $(LOGO_PATH / "logo-icon.png")
  FAVICON_SVG_URL* = $(LOGO_PATH / "logo-icon.svg")
  APPLE_PNG180_URL* = $(LOGO_PATH / "apple-touch-icon.png")
  MAX_DIR_FILES* = 10
# ...

Kesimpulannya

Terdapat banyak perkara yang saya tidak nyatakan, kerana syaitan ada dalam butirannya...namun ini adalah lawatan kasar ke seluruh pangkalan kod yang berjumlah:

Apa yang akan saya lakukan secara berbeza?

Tanda Pos: