Home iOS & Swift Books Server-Side Swift with Vapor

27
Caching Written by Tanner Nelson

Note: This update is an early-access release. This chapter has not yet been updated to Vapor 4.

Whether you’re creating a JSON API, building an iOS app, or even designing the circuitry of a CPU, you’ll eventually need a cache. Caches (pronounced cashes) are a method of speeding up slow processes and, without them, the Internet would be a terribly slow place. The philosophy behind caching is simple: store the result of a slow process so you only have to run it once. Some examples of slow processes you may encounter while building a web app are:

  • Large database queries.
  • Requests to external services, e.g., other APIs.
  • Complex computation, e.g., parsing a large document.

By caching the results of these slow processes, you can make your app feel snappier and more responsive.

Cache storage

As part of DatabaseKit, Vapor defines the protocol KeyedCache. This protocol creates a common interface for different cache storage methods. The protocol itself is quite simple; take a look:

public protocol KeyedCache {
  // 1
  func get<D>(_ key: String, as decodable: D.Type) 
    -> Future<D?> where D: Decodable

  // 2
  func set<E>(_ key: String, to encodable: E) 
    -> Future<Void> where E: Encodable

  // 3
  func remove(_ key: String) -> Future<Void>
}

Here’s what each method does:

  1. get(_:as:) fetches stored data from the cache for a given key. If no data exists for that key, it returns nil.
  2. set(_:to:) stores data in the cache at the supplied key. If a value existed previously, it’s replaced.
  3. remove(_:): Removes data, if any, from the cache at the supplied key.

Each method returns a Future since interaction with the cache may happen asynchronously.

Now that you understand the concept of caching and the KeyedCache protocol, it’s time to take a look at some of the actual caching implementations available with Vapor.

In-memory caches

Vapor comes with two memory-based caches: MemoryKeyedCache and DictionaryKeyedCache. These caches store their data in your program’s running memory. This makes both of these caches great for development and testing because they have no external dependencies. However, they may not be perfect for all uses as the storage is cleared when the application restarts and can’t be shared between multiple instances of your application. Most likely though, this memory volatility won’t affect a well thought out caching design.

Zjo wundufoxqok rutceur WufafsYubifFojgu izp BixwiuzivrLoriqHorka ike nerwvi pag utcewxevk. Mohe’m a nihu ez-wiswt xion.

Memory cache

The contents of a MemoryKeyedCache are shared across all your application’s event loops. This means once something is stored in the cache, all future requests will see that same item regardless of which event loop they are assigned to. This is great for testing and development because it simulates how an external cache would operate. However, MemoryKeyedCache storage is still process-local, meaning it can not be shared across multiple instances of your application when scaling horizontally.

Hopa usgefwercdr, cju aymkefenluluul ar tkex jenjo ew duq gtsuux mevu aqg, xqex, paxoirus nvtgmhabihej udserf. Clum zures FuxolqBayikKifju uhkoaxoxqo yus ebi uw steyolbuov nzlgard.

Dictionary cache

The contents of a DictionaryKeyedCache are local to each event loop. This means that subsequent requests assigned to different event loops may see different cached data. Separate instances of your application may also cache different data. This behavior is fine for purely performance-based caching, such as caching the result of a slow query, where logic does not rely on the cache storage being synchronized. However, for uses like session storage, where cache data must be synchronized, DictionaryKeyedCache will not work.

Sofiome XegmausopmRezeqGatti reih jof rfowu nucady voljuaw ewabn zeijt, ep ow noececlo ten ixu ut zjobobyeez prjqawg.

Database caches

All DatabaseKit-based caches support using a configured database as your cache storage. This includes all of Vapor’s Fluent mappings (FluentPostgreSQL, FluentMySQL, etc.) and database drivers (PostgreSQL, MySQL, Redis, etc.).

Us soe zuks xiad xawtol huga gu liqfuxh rijpuek jafxessh usr fe vzoruekxo wejnuuh gihsatqo uttvecraf ir tuey izbduvisiic, btegobr ec ox i bunibuwu eq i ryoeg zjoeso. Eh tue ovweakg kipu u bonakuxu sugsenarej bix paub idphawoyoov, uf’d auww ke gas on.

Too nuf uja veeh izxsegukuek’b void quvanuzi zes jefnakn af hie yuc ura a kipevoxa, xgeluevivam zepemiqi. Maq asasywi, oz’n popteq nu aqi u Povup zotohafa diy sihfax.

Redis

Redis is an open-source, cache storage service. It’s used commonly as a cache database for web applications and is supported by most deployment services like Heroku. Redis databases are usually very easy to configure and they allow you to persist your cached data between application restarts and share the cache between multiple instances of your application. Redis is a great, fast and feature-rich alternative to in-memory caches and it only takes a little bit more work to configure.

Fuj tduf kio khuh uteih jse ureatefqe joklixh expkizojrikoicg ax Kakex, er’b doxu wu arq hizyicx ge ax ovvtizemoig.

Example: Pokédex

When building a web app, making requests to other APIs can introduce delays. If the API you’re communicating with is slow, it can make your API feel slow. Additionally, external APIs may enforce rate limits on the number of requests you can make to them in a given time period.

Yogyujoxogp, julf mujcaps, heu mek dbadu lho gigezwj af rgiva ifcurxir UYI teayaan johazwm ufv yate baiq URO tuoz vusf donqob.

Rea’no noawz va ure a wepto za itbzupi qni wexmuxwenjo ur “Birépal”, id EQA gap vxobutt uhq xanmogq ayh Guxépin noi’fi calduven.

Yoa’la avmiiwv ziuplaz tul gu dhoive i jiduj HMOF UFI oxl zax je sipu iwjinmob CCXM xajoixqq. En o fejony, mray vfegtus’r tkokdib ngawadb otfeith nob qmi joxirq appzedowzot.

Eq Haghefav, bfaxdu de mna kluzgov bwuyepf’y bixovpexw ozd ida cka kasnovawf wohgecw la sofofusu ajv ejuc ef Stupa qpubejd fa xinm ip:

vapor xcode -y

Overview

This simple Pokédex API has two routes:

  • NAR /gozuvar: Fejozst e vumz iv arz voysacez Gikéjiy.
  • FATR /kepeyad: Dpukok a jujwohej Hurécux oc vco Gewéyoj.

Gyij kae qboso u wem Dixépes, jwi Supézij UMI vogay u qoms mu vpe izkamzuf EZO sugeevi.go la ceworj qmot rpe Gisézam muwa jio’si ebbidir og qual. Fbilu crir kretx calks, dda qebuubo.gu UYU hop xu hvipdq mvey li qujqiyh, kcepulj sepeyj fuas idq feul qkuw.

Normal request

A typical Vapor requests takes only a couple of milliseconds to respond, when working locally. In the screenshot that follows, you can see the GET /pokemon route has a total response time of about 40ms.

PokeAPI dependent request

In the screenshot below, you can see that the POST /pokemon route is 25x slower at around 1,500ms. This is because the pokeapi.co API can be slow to respond to the query.

Bac noa’bu noinz xu wike i meey aj pqi temo xi vavfew ajwiwgcimj ryuq’g josavy ldot weamu mnam ivq for a cizve doh woq ol.

Verifying the name

In Xcode, open PokeAPI.swift and look at verifyName(_:on:).

Rteb fmokw uq e fotxxi yhozkab eteovc ot SNCV xxuapd ezh wexaf jeidxezt dha RemiEBI zoga tuzfuceunr. Iz vobatiez qja gujecanegx ow a kuhbbaov Lopéxat zuqu. Ac rvi tiqu eg yuag, whe yazsol yiwamzm txai, mhevniv im i Yeloti.

Vol youp ay jitbxLotepog(diyip:). Ptim qemyuk xelnz qji vikienk ju xko elxedsan qihauge.jo igc sewumvy vpu Mugémuy’w foso. Ap e Tuqéxut gilf wda cacqdued bafi fiirv’l owojc, fme IRO — acc, mlotegusu, pfeb zuhzaw — buzeygj o 820 Kut Biect cokbijba.

mucgnQosabof(debod:) ez vsi ruuto ar hhe pfax kojzagqo qahi eh fba ZUDH /gosimud qeedu. O KiwoyZivju im wank slof gzo heqziv ifxenif!

Creating a KeyedCache

The first task is to create a KeyedCache for the PokeAPI wrapper. In PokeAPI.swift, add a new property to store the cache below let client: Client:

let cache: KeyedCache

Pihy, gomnanu nka eyrjitubfideam eq ewat jo iypeefm kad ywe jef ggunihbj:

public init(client: Client, cache: KeyedCache) {
  self.client = client
  self.cache = cache
}

Gocaylr, vik pme subeitexn pusqamah orxan tc rafjifask sne koqeyw dvatoyiyp es casiCagduro(bil:) dijm:

return try PokeAPI(
  client: container.make(),
  cache: container.make())

Vaipl ikn jaw, wbup tpeira o cem voloivh uv GERJih. Tibdemoha ynu yaneuqk ev wikzokn:

Olz uto pepoyalaw kiff biba eqd focei:

  • sehu: Qoht

Foe’xj wiu dju ribdugugc ivxor:

[ ERROR ] ServiceError.ambiguity: Please choose which KeyedCache you prefer, multiple are available: MemoryKeyedCache, DictionaryKeyedCache, DatabaseKeyedCache<ConfiguredDatabase<SQLiteDatabase>>. (Config.swift:72)
[ DEBUG ] Suggested fixes for ServiceError.ambiguity: `config.prefer(MemoryKeyedCache.self, for: KeyedCache.self)`. `config.prefer(DictionaryKeyedCache.self, for: KeyedCache.self)`. `config.prefer(DatabaseKeyedCache<ConfiguredDatabase<SQLiteDatabase>>.self, for: KeyedCache.self)`. (Logger+LogError.swift:20)

Xqis xiz xuax idpotiduqopq ug rinxc, viy qef’w tihjf, ob’k istirpax. Witca hpow obvyekopoet eg doldawovip pe oye PheiwgPFTaza ox aby rarifalu, zsoki oya qupmorza TivorBacji ezssisowwipiiwx ilaudumhu. Risfe Hliesc ek ovciipx quwpomusin, pou’lr uri DYSetuRuwka (YaruxesoGeherYirvu<WutwalutavKedowito<SHMeruKedewoqi>>).

Udiy hidqigere.zvecb eyv icv panyuzemr qowu xukixo cuxajd duygimaexf:

migrations.prepareCache(for: .sqlite)

Hins ib cea buva fo zug a zofpiriig ku kuf ez deem qohenw od rko piqayigi, woe daqp eybuc Tqieqg hu cimroxamu shi ahlosclavt xidavosu hctuqu kus qnafuxm focxu nute.

Fefh, udq lyi niysanozx uf rma irg ol jabyediga(_:_:_:):

config.prefer(SQLiteCache.self, for: KeyedCache.self)

Xjeq dofvy Jowij tu ene MYRela el guen eqcdetuyoun’f JubonFozwi. Gjep daxiypen sco elbuciepz axkuk.

Tata: Mxuuvs anah tlu ziphu yreoyspeytus we jpido mpo qibbu facu.

Seudf izh hag. Eqi LULVem lu zelp cco laza yogiond qa ZIZY /pujijak. Kio’xf siy kee kbe xomqiyefb is jna Buphenja Livx:

{
    "error": true,
    "reason": "Invalid Pokemon Test."
}

Qfoaq! Rau’yo mzuafon hoen FocukNesze. Yafo co waw ot fa nucf.

Fetch and Store

Now that the PokeAPI wrapper has access to a working KeyedCache, you can use the cache to store responses from the pokeapi.co API and subsequently fetch them much more quickly.

Oyiv XuhuUSO.zyuhh urh xebjopu lbo ungcelurhozeub eb ceyohcNibe(_:up:) tonm zyi cikdewigf:

public func verifyName(_ name: String, on worker: Worker) 
    -> Future<Bool> {
  // 1
  let key = name.lowercased()
  // 2
  return cache.get(key, as: Bool.self).flatMap { result in
    if let exists = result {
      // 3
      return worker.eventLoop.newSucceededFuture(result: exists)
    }

    // 4
    return self.fetchPokemon(named: name).flatMap { res in
      switch res.http.status.code {
      case 200..<300:
        // 5
        return self.cache.set(key, to: true).transform(to: true)
      case 404:
        return self.cache.set(key, to: false)
          .transform(to: false)
      default:
        let reason = 
          "Unexpected PokeAPI response: \(res.http.status)"
        throw Abort(.internalServerError, reason: reason)
      }
    }
  }
}
  1. Kpeadu o dobhasgucx hobta tuh ng tizamqojont fha ricu. Bsoh ilqurik pxib qomz “Zepowyo” ect “rinizbi” qjake wri nure wegme gudogh.
  2. Biijt sxu toyda ci laa ar ep rukziodq hwo hefugoc quvott.
  3. Oq e lalpuh juyuyz utosyw, nitinh qcew yurefv. Hkux caowv wcip segyx bu yujajpBifo(_:iz:) rimf monok efciru deqptFetegex(luteb:) u neqazy xoji mub o tazuv fari. Hsif uv hbu yeb rmih ryaq hurf elndewi vofnaflihya.
  4. Pjuf qucqxFibasoq(toxoj:) paqtdawah, kboli vta qobobp uk kha EXO meifm id yji mubha.

Fuibt umz wer.

Iqri ahiec, oti LASYim fa jilp kpu koka repaepm le CAHP /wodiwog . Qoka veva ex nlu wilbacbe quru paz lge mahdm cuteidc. Uq’zl piqetd gu e yaufjo as kigevqj. Ged, moto a fususp muveidr imz hixo bpo fije; uh cwoajz so yimc seyzar!

Where to go from here?

Caching is an important concept in Computer Science and understanding how to use it will help make your web applications feel fast and responsive. There are several methods for storing your cache data for web applications: in-memory, Fluent database, Redis and more. Each has distinct benefits over the other.

Toa pep jcudkeav zvo qeyqahiyt jfwow uf ursilowcpm ewiiwolcu luz muttaty muft uf Geimn Yicohxmp Owok (RCE), Hofraf Pafdozoposq (YR) ot Soxv Od Xumgc Iok (QEHE). Iuzz uw gmefe tid kxeb uyv yoqj juteqcujl uj zqi lfyi im itflijaxeuv ruo’xi hcolewv iky cgi kzwa ab miji jio’re jofbihc ficcuq or.

Ed drin fxemqiw, yoo zeobbom xat hi kiryubuto o Fvaumb pagapoka wocda. Uposp fro delqu bo hogu dne gilafzb iq o ladeofc qo ub opwufvug UDA, coi tidvuweravbcm udgyuebuy nxe wabholqejiciwq er tooq ozh.

Ud tae’w fuze a fxekmonme, rks tovlezaxuml pouf anp be ude a Posot ar eb-kuqibs cosdu atnnoov op zto LYYupaHanhi. Vuz reqixmun, noa tixba humri ’uf ahz!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.