Простой макрос кэширования Racket

Проект, над которым я работаю, по созданию пользовательского веб-сайта, предполагает работу с API третьей стороны для получения большого количества информации. API связывает пользователей с продуктами, привязанными к их аккаунту, и API может предоставить вам дополнительную информацию о каждом продукте в отдельных вызовах API.

Чтобы полностью создать этот проект, мне нужна информация обо всех пользователях, продуктах, которыми они владеют, а затем дополнительная информация о каждом продукте, чтобы создать информативный сайт. У нас есть N пользователей и M продуктов, и есть ряд связей между пользователями и продуктами.

Чтобы начать загрузку проекта, мы перебираем пользователей, затем перебираем все продукты, которыми они владеют.

(for ([user all-users])
  (define users-products ($fetch-products (user-id user)))
  (for ([pid users-products])
    (define product-info ($fetch-product pid))
    (do-something-with product-info)))
Вход в полноэкранный режим Выход из полноэкранного режима

Это не очень хорошо, потому что если у нас 500 пользователей и у каждого пользователя 1000 продуктов, то мы совершим около 500 000 сетевых вызовов. Я не думаю, что поставщик API будет сильно об этом беспокоиться. Вместо этого мы должны принять во внимание, что в то время как информация о пользователях может регулярно меняться, информация о продуктах часто не меняется.

Давайте вместо этого хранить информацию о продукте в нашей файловой системе в виде плоских файлов. Таким образом, когда мы создаем новые сборки нашего сайта, у нас уже есть продукты, и мы все еще можем получать данные о пользователях из API, поскольку они могут часто меняться.

(for ([user all-users])
  (define users-products ($fetch-products (user-id user)))
  (for ([pid users-products])
    (define cached (build-path "cache" (format "~a.txt" pid)))
    (define product-info
      (if (file-exists? cached)
          (file->string cached)
          (let ([resp ($fetch-product pid)])
            (call-with-output-file cached #:exists 'replace
              (lambda (out)
                (write resp out)))
             resp)))
    (do-something-with product-info)))
Вход в полноэкранный режим Выход из полноэкранного режима

Это довольно многословно. Эта программа в ее нынешнем виде не очень модульная, поскольку кэширование теперь связано с логикой нашей основной программы. Внесение изменений в основную программу означает «ныряние» в код кэширования и обратно, что не очень хорошо отражает наши истинные намерения. Мы должны перенести логику кэширования в отдельную функцию для модульности и читабельности.

(define (Cachify path-check code-to-run)
  (if (file-exists? path-check)
      (file->string path-check)
      (let ([resp code-to-run])
        (call-with-output-file cached
          #:exists 'replace
          (lambda (out)
            (write resp out)))
         resp)))
Вход в полноэкранный режим Выход из полноэкранного режима

На мой взгляд, это выглядит правильно. Мы проверяем путь к файлу, и если файла нет, мы выполняем наш код полезной нагрузки. Вроде бы пока все правильно.

Но здесь кроется наша следующая проблема: из-за того, как Racket оценивает код, попытка использовать это приведет к тому, что мы будем выполнять сетевой запрос каждый раз, независимо от того, был ли он сохранен в файл или нет.

> (define product-info (Cachify "product.txt" ($fetch-product 5))
; ... still runs the network request ...
> "{"pid":"5"}"
Вход в полноэкранный режим Выйти из полноэкранного режима

Это явно не подходит для нас. Нам нужно перевести это в макрос на уровне языка, где Racket будет оценивать макрос, как если бы это была обычная функция Racket, но рассматривает аргументы как литеральные значения, а не как подвыражения для оценки. К счастью, это не сильно отличается.

(define-syntax-rule (Cachify path-check code-to-run)
  (if (file-exists? path-check)
      (file->string path-check)
      (let ([resp code-to-run])
        (call-with-output-file path-check
          #:exists 'replace
          (lambda (out)
            (write resp out)))
         resp)))
Вход в полноэкранный режим Выйти из полноэкранного режима

Этот макрос работает, выполняя буквальный перевод полученного кода и подставляя соответствующие переменные. Если мы попытаемся запустить этот макрос, то увидим, что он заменит код таким образом, что запустит наш сетевой запрос, только если он не находится в файловой системе первым, а не раньше, чем у него появится шанс проверить. Мы можем просмотреть преобразование макроса, используя expand и syntax->datum, чтобы получить более полную картину.

> (syntax->datum (expand '(Cachify "1.txt" (println "Hello"))))
'(if (#%app file-exists? '"1.txt")
   (let-values (((temp16) '"1.txt"))
     (if (#%app
          variable-reference-constant?
          (#%variable-reference file->string101))
       (#%app file->string 'binary temp16)
       (#%app file->string101 temp16)))
   (let-values (((resp) (#%app println '"Hello")))
     (let-values (((string:5:8) call-with-output-file36)
                  ((temp17) '"1.txt")
                  ((temp18) 'replace)
                  ((temp19) (lambda (out) (#%app write resp out))))
; ...
Вход в полноэкранный режим Выход из полноэкранного режима

Это выглядит немного запутанно, но именно так это выглядит внутри виртуальной машины Racket. Сначала она проверяет, существует ли файл, с помощью (if (#%app file-exists? '"1.txt"), и если это не удается, она перемещается вниз, чтобы выполнить выражение, которое мы дали ей в строке (let-values (((resp) (#%app println '"Hello"))).

Это работает не только с сетевыми запросами, но и с откровенно любыми вычислениями, которые вы делаете, вы можете использовать этот макрос. Самое приятное, что вы можете изменить сам макрос для поддержки более современных технологий, например, если вы хотите использовать базу данных или что-то вроде redis в качестве хранилища памяти. Вы можете изменить макрос и продолжать использовать его без необходимости менять реализацию кэширования во всей вашей программе. Теперь давайте создадим наш сайт.

(for ([user all-users])
  (define users-products ($fetch-products (user-id user)))
  (for ([pid users-products])
    (define product-info
      (Cachify (build-path "cache" (format "~a.txt" pid))
               ($fetch-product pid)))
    (do-something-with product-info)))
Вход в полноэкранный режим Выход из полноэкранного режима

Во всех инкрементальных сборках вы сэкономите тысячи обращений к API-хосту, пока информация о продукте не изменится. Если это произойдет, вам, возможно, придется сделать кэш недействительным, очистив старые файлы или что-то подобное, или создать цикл обновления, при котором кэш будет подтверждать себя только в определенные дни или интервалы времени. Но это, конечно, зависит от вас.

Спасибо за чтение!

Оцените статью
devanswers.ru
Добавить комментарий