🏫 The School›📮 CI/CD›⚡ धडा 04 — जलद pipelines: cache, parallel jobs, matrix
🖼️ See the drawing + lab 🏠 Course home 🌿 Branch on GitHub ✏️ View source
🖼️ आकृती आणि labThe drawing + lab पूर्ण पानावर उघडा ↗Open full page ↗

⚡ धडा 04 — जलद pipelines: cache, parallel jobs, matrix

📍 तुम्ही इथे आहात: 12 पैकी धडा 04 · मागे: lesson-03-first-pipeline · पुढे: lesson-05-artifacts-reports


📦 या ब्रँचमध्ये काय आहे

धडे 01–03, आणि courier ची फेरी छोटी ठेवणारी तीन साधने: inputs वरून किल्ली दिलेला ड्रॉवर, एकाच वेळी अनेक डेस्क, आणि तीन प्रकारे नक्कल केलेला एक डेस्क. खऱ्या files:

🧒 5 वर्षांच्या मुलाला समजावल्यासारखे

प्रत्येक फेरीत, तपासणी डेस्कवरचा कारकून तुमच्या गृहपाठाला हात लावण्याआधी पेन्सिलींचा नवा बॉक्स तासायचा. त्याच पेन्सिली, तेच शार्पनर. ✏️

म्हणून mailroom ने एक ड्रॉवर 🗄️ जोडला. त्याचे label "पेन्सिली" नाही — ते तासण्याच्या सूचनांचा ठसा आहे. त्याच सूचना → तेच label → ड्रॉवर उघडा, पेन्सिली घ्या, तासणे वगळा: cache hit ⚡. नव्या सूचना → नवे label → असा ड्रॉवर नाही → तासा, मग नव्या पेन्सिली नव्या label खाली ठेवा: cache miss ⏳. रात्री शिपायाने ड्रॉवर रिकामा केला तरी काहीच हरवत नाही — पुढची फेरी पुन्हा तासते. धडा 05 च्या पाकिटापेक्षा 📎 हाच संपूर्ण फरक आहे.

वेग वाढवण्याच्या आणखी दोन युक्त्या. एकाच वेळी अनेक डेस्क 🪑🪑: एकमेकांवर अवलंबून नसलेले jobs शेजारी शेजारी चालतात. आणि तीन फूटपट्ट्या 📏: तोच डेस्क तीनदा नक्कल केलेला, प्रत्येक वेगळ्या Node version ने मोजणारा. fail-fast बंद असेल तर एकाने चुकीचे मोजले तरी तीनही पूर्ण होतात.

🗺️ आकृती

flowchart LR
    src["📄 app/prepare.js"]
    key["🔑 key<br/>prepared-hash of prepare.js"]
    look["🗄️ drawer lookup"]
    hit["⚡ HIT<br/>restore app/prepared, skip prepare"]
    miss["⏳ MISS<br/>node prepare.js pays the hash rounds"]
    save["💾 save under the key<br/>when the job ends"]
    test["✅ node --test<br/>three rulers at once"]
    src -->|"1 hashFiles"| key
    key -->|"2 look up"| look
    look -->|"3a found"| hit
    look -->|"3b not found"| miss
    miss -->|"4 post step"| save
    hit -->|"5 then test"| test
    miss -->|"5 then test"| test

❓ काय

🧠 Cache करायचे, की नाही?

🗄️ cache करा 🚫 cache करू नका
key contents चे पूर्ण वर्णन करते का? हो — prepared-<hash of app/prepare.js> नाही — hash बाहेरच्या files वरही अवलंबून असणारे outputs
ते हरवले तर किंमत काय वेळ अचूकपणा, किंवा हरवलेली deliverable
उदाहरणे ~/.npm, app/prepared, Docker layers secrets, संपूर्ण repo चा build output, नंतरच्या job ला लागणाऱ्या files (artifact, धडा 05)

🤔 का

सावकाश pipeline कडे दुर्लक्ष होते: लोक commits एकत्र करतात, PR check वगळतात, किंवा "ती अजून चालू असतानाच" merge करतात. सामान्य run चा बराचसा भाग तुमच्या tests चा नसतो — नव्या मशीनवर त्याच गोष्टी पुन्हा तयार करण्याचा असतो. प्रामाणिक key असलेला cache तो भाग काढून टाकतो; matrix एकाच्या वेळेत तीन तपासण्या चालवते; fail-fast: false "Node 24 बिघडले" चे रूपांतर पूर्ण अहवालात करते. Inputs वरून key देण्याचा हाच नियम Docker चा layer cache चालवतो — Docker शाळेचा धडा 02 पाहा.

🔧 कसे (या repo मध्ये)

ड्रॉवर. ci.yml मध्ये cache step key अस्तित्वात असेल तर app/prepared restore करते आणि, miss झाल्यावर, job ची शेवटची step चालल्यानंतर post step मध्ये ते save करते:

      - name: 🗄️ cache the slow preparation (lesson 04)
        id: cache
        uses: actions/cache@v4
        with:
          path: app/prepared
          key: prepared-${{ hashFiles('app/prepare.js') }}   # change prepare.js → new key → miss

      - name: ⏳ prepare (slow on a miss, skipped on a hit)
        if: steps.cache.outputs.cache-hit != 'true'
        run: node prepare.js

id: cache मुळे पुढची step cache-hit वाचू शकते. Hit झाल्यावर सावकाश step पूर्णपणे वगळली जाते; miss झाल्यावर ती prepare.js चालवते, जी मुद्दाम सावकाश आहे:

const ROUNDS = Number(process.env.PREPARE_ROUNDS || 12_000_000);  // ~3 s on a laptop, ~8–10 s on a CI runner

if (fs.existsSync(OUT)) {
  console.log(`⚡ prepared/table.json already exists — nothing to do (this is what a cache hit feels like)`);
  process.exit(0);
}

ते वेळ ढोबळ अंदाज म्हणून घ्या — hosted runner सहसा laptop पेक्षा सावकाश असतो आणि प्रत्येक run मध्ये बदलतो; महत्त्वाचा आहे तो आकार. existsSync तपासणी लक्षात घ्या: prepare.js disk वर जे असेल त्यावर विश्वास ठेवते, म्हणूनच या cache ला restore keys नाहीत. restore-keys: prepared- पर्याय (उदाहरणापुरता, ci.yml मध्ये नाही) जवळपासच्या miss ला जुनी table.json देईल आणि prepare.js म्हणेल "already exists". Restore keys npm ci सारख्या अर्धवट निकाल पूर्ण करणाऱ्या steps साठी असतात.

तीन फूटपट्ट्या. Matrix job वर असते; प्रत्येक भाग स्वतःच्या runner वरचा पूर्ण job आहे, म्हणून तीनही शेजारी शेजारी चालतात:

    strategy:
      fail-fast: false      # lesson 04: let all three rulers finish, even if one fails
      matrix:
        node: [20, 22, 24]  # three rulers — the same homework checked three ways

fail-fast: false नसेल (default true आहे) तर पहिला लाल भाग बाकीचे दोन रद्द करतो; compatibility matrix ला तीनही उत्तरे हवी असतात. याउलट ship.yml पाहा, ज्याचे jobs needs: ने साखळीत जोडलेले आहेत — वेगवेगळे काम, मुद्दाम parallel नाही.

Docker layers हा इथला दुसरा मोठा cache आहे: नव्या runner कडे एकही नसतो, म्हणून धडा 07 मधला docker build प्रत्येक layer पुन्हा build करतो, जोपर्यंत तुम्ही cache सोबत आणत नाही (पर्याय आहेत; layer cache काय पुन्हा वापरू शकतो आणि काय नाही हे Docker शाळेचा धडा 02 सांगतो).

🔁 हेच CircleCI मध्ये
      - restore_cache:                                        # lesson 04: the drawer
          keys:
            - prepared-v1-{{ checksum "prepare.js" }}
      - run:
          name: ⏳ prepare (skipped when restored from cache)
          command: node prepare.js
      - save_cache:
          key: prepared-v1-{{ checksum "prepare.js" }}
          paths: [prepared]

Restore आणि save या स्पष्ट steps आहेत; prepare.js प्रत्येक फेरीत चालते आणि hit झाल्यावर फक्त "already exists" छापते. v1- हा हाताने reset करण्याचा switch आहे; matrix workflows: खाली असते.

🦊 हेच GitLab CI मध्ये
  parallel:
    matrix:                                   # lesson 04: three rulers
      - NODE: ["20", "22", "24"]
  cache:                                      # lesson 04: the drawer, keyed by the file that defines the work
    key:
      files: [app/prepare.js]
    paths: [app/prepared/]

cache: key: files: यादीतील files चा hash तुमच्यासाठी काढते (hashFiles ची कल्पना); restore आणि save script: च्या आजूबाजूला आपोआप होतात.

🎩 हेच Jenkins मध्ये
      matrix {                                         // lesson 04: three rulers, in parallel
        axes { axis { name 'NODE'; values '20', '22', '24' } }
        agent { docker { image "node:${NODE}-alpine" } }
                // Jenkins has no built-in cache step: a persistent agent keeps the workspace
                // (so prepared/ survives between builds); ephemeral agents need the Job Cacher plugin.
                sh 'node prepare.js'

प्रामाणिक टीप: matrix directive built-in आहे; key असलेला cache नाही.

🧪 करून पाहा

# 0) feel a miss and a hit on your laptop
cd app
node prepare.js            # ⏳ preparing… ✅ wrote prepared/table.json in N s (a few seconds, laptop-dependent)
node prepare.js            # ⚡ already exists — nothing to do
git status --short         # empty: app/prepared/ is git-ignored, so the drawer stays out of commits
cd ..

# 1) in your fork: a whitespace change to prepare.js → new hash → new key → MISS
latest() { gh run list --workflow ci.yml --limit 1 --json databaseId --jq '.[0].databaseId'; }
echo "" >> app/prepare.js && git commit -am "touch prepare.js (new cache key)" && git push
gh run watch "$(latest)" && gh run view "$(latest)" --log | grep -E "Cache not found|preparing|Cache saved"

# 2) push again WITHOUT touching prepare.js → same key → HIT, prepare step skipped
git commit --allow-empty -m "same key, expect a hit" && git push
gh run watch "$(latest)" && gh run view "$(latest)" --log | grep -E "Cache restored"

# 3) see the drawers GitHub keeps for your fork
gh cache list

⚠️ नेहमीच्या चुका

⏭️ पुढे

ड्रॉवर पुन्हा बनवता येणाऱ्या गोष्टींसाठी आहे. पुढे: ज्या गोष्टी तुम्ही हरवू देऊ नयेत त्या — माणूस वाचतो ते प्रगतिपुस्तक 📋 आणि पुढचा डेस्क उघडतो ते पाकीट 📎.

git checkout lesson-05-artifacts-reports

⚡ Lesson 04 — Fast pipelines: cache, parallel jobs, matrix

📍 You are here: Lesson 04 of 12 · Previous: lesson-03-first-pipeline · Next: lesson-05-artifacts-reports


📦 What's in this branch

Lessons 01–03, plus the three tools that keep the courier's round short: a drawer keyed by its inputs, several desks at once, and one desk cloned three ways. Real files:

🧒 Explain like I'm 5

Each round, the clerk at the checking desk used to sharpen a fresh box of pencils before touching your homework. Same pencils, same sharpener. ✏️

So the mailroom added a drawer 🗄️. Its label is not "pencils" — it is a fingerprint of the sharpening instructions. Same instructions → same label → open the drawer, take the pencils, skip the sharpening: a cache hit ⚡. New instructions → new label → no such drawer → sharpen, then file the fresh pencils under the new label: a cache miss ⏳. If the janitor empties the drawer overnight, nothing is lost — the next round sharpens again. That is the whole difference from the envelope 📎 of lesson 05.

Two more speed tricks. Several desks at once 🪑🪑: jobs that do not depend on each other run side by side. And three rulers 📏: the same desk cloned three times, each measuring with a different Node version. With fail-fast off, all three finish even if one measures wrong.

🗺️ Diagram

flowchart LR
    src["📄 app/prepare.js"]
    key["🔑 key<br/>prepared-hash of prepare.js"]
    look["🗄️ drawer lookup"]
    hit["⚡ HIT<br/>restore app/prepared, skip prepare"]
    miss["⏳ MISS<br/>node prepare.js pays the hash rounds"]
    save["💾 save under the key<br/>when the job ends"]
    test["✅ node --test<br/>three rulers at once"]
    src -->|"1 hashFiles"| key
    key -->|"2 look up"| look
    look -->|"3a found"| hit
    look -->|"3b not found"| miss
    miss -->|"4 post step"| save
    hit -->|"5 then test"| test
    miss -->|"5 then test"| test

❓ What

🧠 Cache it, or not?

🗄️ cache it 🚫 do not cache it
does the key fully describe the contents? yes — prepared-<hash of app/prepare.js> no — outputs that also depend on files outside the hash
what losing it costs time correctness, or a missing deliverable
examples ~/.npm, app/prepared, Docker layers secrets, whole-repo build output, files a later job needs (an artifact, lesson 05)

🤔 Why

A slow pipeline gets ignored: people batch commits, skip the PR check, or merge "while it's still running". Much of a typical run is not your tests — it is preparing the same things again on a fresh machine. A cache with an honest key removes that part; a matrix runs three checks in the time of one; fail-fast: false turns "Node 24 broke" into a full report. The same key-by-inputs rule drives Docker's layer cache — see the Docker school's lesson 02.

🔧 How (in this repo)

The drawer. In ci.yml the cache step restores app/prepared if the key exists and, after a miss, saves it in a post step once the job's last step has run:

      - name: 🗄️ cache the slow preparation (lesson 04)
        id: cache
        uses: actions/cache@v4
        with:
          path: app/prepared
          key: prepared-${{ hashFiles('app/prepare.js') }}   # change prepare.js → new key → miss

      - name: ⏳ prepare (slow on a miss, skipped on a hit)
        if: steps.cache.outputs.cache-hit != 'true'
        run: node prepare.js

id: cache lets the next step read cache-hit. On a hit the slow step is skipped entirely; on a miss it runs prepare.js, which is slow on purpose:

const ROUNDS = Number(process.env.PREPARE_ROUNDS || 12_000_000);  // ~3 s on a laptop, ~8–10 s on a CI runner

if (fs.existsSync(OUT)) {
  console.log(`⚡ prepared/table.json already exists — nothing to do (this is what a cache hit feels like)`);
  process.exit(0);
}

Treat those timings as a rough guide — a hosted runner is usually slower than a laptop and varies run to run; the shape is what matters. Note the existsSync check: prepare.js trusts whatever is on disk, which is why this cache has no restore keys. A restore-keys: prepared- fallback (illustrative, not in ci.yml) would hand a near-miss a stale table.json and prepare.js would say "already exists". Restore keys belong on steps like npm ci that finish a partial result.

Three rulers. The matrix lives on the job; each leg is a full job on its own runner, so the three run side by side:

    strategy:
      fail-fast: false      # lesson 04: let all three rulers finish, even if one fails
      matrix:
        node: [20, 22, 24]  # three rulers — the same homework checked three ways

Without fail-fast: false (the default is true) the first red leg cancels the other two; a compatibility matrix wants all three answers. Contrast ship.yml, whose jobs are chained with needs: — different work, deliberately not parallel.

Docker layers are the other big cache here: a fresh runner has none, so docker build in lesson 07 rebuilds each layer unless you bring a cache along (options exist; the Docker school's lesson 02 covers what a layer cache can and cannot reuse).

🔁 The same thing in CircleCI
      - restore_cache:                                        # lesson 04: the drawer
          keys:
            - prepared-v1-{{ checksum "prepare.js" }}
      - run:
          name: ⏳ prepare (skipped when restored from cache)
          command: node prepare.js
      - save_cache:
          key: prepared-v1-{{ checksum "prepare.js" }}
          paths: [prepared]

Restore and save are explicit steps; prepare.js runs each round and just prints "already exists" on a hit. v1- is a manual reset switch; the matrix sits under workflows:.

🦊 The same thing in GitLab CI
  parallel:
    matrix:                                   # lesson 04: three rulers
      - NODE: ["20", "22", "24"]
  cache:                                      # lesson 04: the drawer, keyed by the file that defines the work
    key:
      files: [app/prepare.js]
    paths: [app/prepared/]

cache: key: files: hashes the listed files for you (the hashFiles idea); restore and save are implicit around script:.

🎩 The same thing in Jenkins
      matrix {                                         // lesson 04: three rulers, in parallel
        axes { axis { name 'NODE'; values '20', '22', '24' } }
        agent { docker { image "node:${NODE}-alpine" } }
                // Jenkins has no built-in cache step: a persistent agent keeps the workspace
                // (so prepared/ survives between builds); ephemeral agents need the Job Cacher plugin.
                sh 'node prepare.js'

Honest note: the matrix directive is built in; a keyed cache is not.

🧪 Try it

# 0) feel a miss and a hit on your laptop
cd app
node prepare.js            # ⏳ preparing… ✅ wrote prepared/table.json in N s (a few seconds, laptop-dependent)
node prepare.js            # ⚡ already exists — nothing to do
git status --short         # empty: app/prepared/ is git-ignored, so the drawer stays out of commits
cd ..

# 1) in your fork: a whitespace change to prepare.js → new hash → new key → MISS
latest() { gh run list --workflow ci.yml --limit 1 --json databaseId --jq '.[0].databaseId'; }
echo "" >> app/prepare.js && git commit -am "touch prepare.js (new cache key)" && git push
gh run watch "$(latest)" && gh run view "$(latest)" --log | grep -E "Cache not found|preparing|Cache saved"

# 2) push again WITHOUT touching prepare.js → same key → HIT, prepare step skipped
git commit --allow-empty -m "same key, expect a hit" && git push
gh run watch "$(latest)" && gh run view "$(latest)" --log | grep -E "Cache restored"

# 3) see the drawers GitHub keeps for your fork
gh cache list

⚠️ Common mistakes

⏭️ Next

The drawer is for things you could remake. Next: the things you must not lose — the report card 📋 a human reads and the envelope 📎 the next desk opens.

git checkout lesson-05-artifacts-reports
← Previousfirst pipelineNext →artifacts reports

This page is the lesson's README from the lesson-04-cache-parallel-matrix branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.