Milestones: M1 Through M9
Milestones are the "what does mastery look like at this stage" checkpoints. They map to the nine levels and the 16-week plan, but they are the real gate — the calendar is only a suggestion. Each milestone has:
- Expected completion — a calendar guideline tied to the 16-week plan.
- Skills you must demonstrate — 5–8 concrete abilities, tied to OpenSearch internals.
- Self-check questions — answer them out loud, without notes.
- 20-point rubric — five criteria, four points each.
- Pass threshold — minimum total to advance.
- Move to the next level when — the binary gate.
Pass thresholds are deliberately high. The point is competence, not throughput. A maintainer-track contributor is measured in months of sustained, high-quality work — these milestones are how you know you are on track.
M1 — Orientation and Data Model (end of Week 2)
You can run OpenSearch as a user, and you can state precisely what is OpenSearch and what is Lucene.
Skills
- Build OpenSearch from a clean checkout and serve
:9200via./gradlew run. - Reproduce all five warm-up scenarios with
curlagainstlocalhost:9200. - Draw the data model: cluster → index → shard (primary/replica) → Lucene index → segment.
- Classify any concept as OpenSearch-owned vs. Lucene-owned (scoring, DocValues, translog, cluster state, REST/transport).
- Write a mapping and explain each field type's Lucene representation.
- Run a single test by name:
./gradlew :server:test --tests "<Class>.<method>". - Locate any
org.opensearch.*class inserver/within 60 seconds via Go to Class orgrep.
Self-check questions
- Which of these is Lucene and which is OpenSearch: BM25 scoring, the translog, the inverted index,
the
RoutingTable, segment merges? - What is the difference between a
textand akeywordfield, and why does it matter for aggregations? - What does
_refreshmake happen, and how is it different from_flush?
Rubric
| Criterion | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| Build/run fluency | ./gradlew run works | Runs scenarios | Runs a named test | Diagnoses a run failure |
| Data model | Names the terms | Sketches index→shard | Sketches down to segments | Predicts shard layout |
| OS-vs-Lucene boundary | Confused | Knows a few | Classifies most | Classifies any concept |
| REST/curl fluency | Copies examples | Edits queries | Writes from memory | Predicts the response shape |
| Communication | Cannot explain | Explains with notes | Explains without notes | Teaches another |
Pass threshold: 14/20, with no criterion below 2.
Move to Level 2 when: from a verbal prompt, you can index sample data and run a filtered search +
a date_histogram aggregation from memory, and state the OS-vs-Lucene boundary cold.
M2 — Build and Test Literacy (end of Week 4)
You can navigate the codebase, build it, run any test, and produce a contribution-ready commit.
Skills
- Run a single test in any module:
./gradlew :server:test --tests "Class.method". - Add a new test file and have Gradle pick it up.
- Run and interpret
./gradlew precommit(checkstyle, forbidden APIs, license headers,loggerUsageCheck). - Identify the module of a class from its FQN (
org.opensearch.cluster...→server;org.opensearch.core...→libs/core). - Produce a commit with a DCO
Signed-off-by:line (git commit -s) and aCHANGELOG.mdentry. - Distinguish a unit test (
*Tests) from an integration test (*IT,internalClusterTest). - Describe the full PR lifecycle: fork → branch → sign-off → CHANGELOG → PR → DCO/precommit → review → backport label.
Self-check questions
- Why is there no JIRA and no CLA — what replaces them in OpenSearch?
- What does every PR have to include besides code, and which check enforces the sign-off?
- What is the difference between
./gradlew :server:testand./gradlew :server:internalClusterTest?
Rubric
| Criterion | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| Build mastery | assemble works | Knows localDistro/run | Knows module deps | Diagnoses build failures |
| Test execution | Runs all | Runs a class | Runs a method | Runs internalClusterTest |
| Precommit | Unaware | Runs it | Reads failures | Fixes checkstyle/headers |
| Contribution hygiene | None | Signs off | Adds CHANGELOG | Fills PR template correctly |
| Module map | Knows names | Knows top-level deps | Maps FQN→module | Diagnoses where a class lives |
Pass threshold: 14/20.
Move to Level 3 when: on a fresh checkout you can build, run a :server:test method by name, and
produce a signed, CHANGELOG-bearing commit that passes ./gradlew precommit — within 20 minutes.
M3 — The Request Path (end of Week 6)
You can trace a request from the REST handler through the transport action framework.
Skills
- Trace
_searchend to end:RestController→RestSearchAction→NodeClient.execute→TransportSearchAction, with a breakpoint at each hop. - Explain
ActionModuleregistration: how aRestHandlerand aTransportActionget wired. - Distinguish the transport action base classes (
HandledTransportAction,TransportSingleShardAction,TransportBroadcastAction,TransportReplicationAction,TransportClusterManagerNodeAction). - Describe how a
TransportRequestis serialized (Writeable+StreamInput/StreamOutput,NamedWriteableRegistry) and moved byTransportService/Netty4Transport. - Name three thread pools (
SEARCH,WRITE,GET, …) and the work each owns. - Build a custom REST action plugin and serve it on
:9200.
Self-check questions
- When a request must run on the elected cluster manager, which base action routes it there?
- How does a polymorphic transport payload deserialize on the receiving node?
- Which thread pool does a
_searchrun on, and why does that matter under load?
Rubric
| Criterion | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| REST→action path | Vague | Names the chain | Cites files | Walks it with breakpoints |
| Action framework | Confused | Knows base classes | Picks the right one | Knows routing/replication semantics |
| Serialization | Unaware | Knows Writeable | Knows StreamInput/Output | Knows NamedWriteableRegistry |
| Threadpools | Unaware | Names a few | Maps work→pool | Reasons about saturation |
| Plugin | Cannot | Stub compiles | Responds on :9200 | Idiomatic + tested |
Pass threshold: 14/20.
Move to Level 4 when: you can answer "where does my _search request first leave the REST layer
and enter server code?" with a file:method citation, and your custom REST action responds on :9200.
M4 — Coordination and Cluster State (end of Week 8)
You understand cluster-manager election, the cluster state, its publishing, and shard allocation.
Skills
- Name the four components of
ClusterState(Metadata,RoutingTable,DiscoveryNodes,ClusterBlocks) and what each holds. - Explain the two-phase publish/commit and which class publishes
(
PublicationTransportHandler) vs. applies (ClusterApplierService). - Trace a
ClusterStateUpdateTaskthroughMasterServiceto a published, applied state. - Read
_cluster/allocation/explainand name theAllocationDeciderthat blocked an allocation. - Describe election and failure detection (
Coordinator,PreVoteCollector,FollowersChecker,LeaderChecker) and what happens to writes during a re-election. - Implement a
ClusterStateListenerand explain exactly when it fires.
Self-check questions
- Why is
ClusterStateimmutable and versioned? What breaks if two nodes apply different versions? - Which service computes new states and which applies them — and why are they separate?
- What turns a cluster yellow, and what sequence of events brings it back to green?
Rubric
| Criterion | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| Cluster state | Names it | Knows the 4 parts | Reads _cluster/state | Predicts a state diff |
| Publish/apply | Confused | Knows the split | Knows two-phase | Walks it in source |
| Coordination | Aware | Names Coordinator | Knows election flow | Reasons about split-brain safety |
| Allocation | Black box | Names deciders | Reads allocation/explain | Diagnoses + fixes a decider |
| Listener/update | Cannot | Stub fires | Correct timing | Batched update task understood |
Pass threshold: 16/20 — this is the first hard gate.
Move to Level 5 when: given an UNASSIGNED shard, you can use _cluster/allocation/explain to
name the responsible decider, and you have a working ClusterStateListener with a test.
M5 — Testing and the InternalTestCluster (end of Week 10)
You can write multi-node in-JVM tests, reproduce randomized failures, and handle flaky tests correctly.
Skills
- Write an
OpenSearchIntegTestCasethat spins up a multi-nodeInternalTestClusterin-JVM. - Write an
OpenSearchSingleNodeTestCaseand a plainOpenSearchTestCaseunit test. - Reproduce a randomized failure from its printed
-Dtests.seed=...line. - Write an
AbstractWireSerializingTestCase/AbstractSerializingTestCaseround-trip for aWriteable/XContent type. - Mute a flaky test correctly with
@AwaitsFix(bugUrl="https://github.com/opensearch-project/OpenSearch/issues/NNNN")— never@Ignore. - Take a
flaky-test-labeled issue from reproduction toward a candidate fix.
Self-check questions
- Why are OpenSearch tests randomized, and how do you make a failure deterministic again?
- Why does serialization round-trip testing matter for backward compatibility?
- What is the wrong way to silence a flaky test, and why is it wrong?
Rubric
| Criterion | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| Integ tests | Runs them | Writes single-node | Writes multi-node | Controls cluster scope |
| Randomization | Confused | Knows seeds | Reproduces a failure | Minimizes a repro |
| Serialization tests | Unaware | Knows the base class | Writes a round-trip | Catches a BWC break |
| Flaky discipline | @Ignore | Knows @AwaitsFix | Links the issue | Roots out the cause |
| Debugging | Reads stack | Maps to source | Reproduces locally | Writes a regression test |
Pass threshold: 15/20.
Move to Level 6 when: you can reproduce a randomized failure from a seed and write a multi-node integration test that asserts cluster behavior — both from memory.
M6 — Indexing Path and the Engine (end of Week 12)
You can read the write path from a transport action down to Lucene and the translog.
Skills
- Walk indexing end to end:
TransportShardBulkAction→IndexShard.applyIndexOperationOnPrimary→InternalEngine.index→ LuceneIndexWriter+Translog.add. - Explain refresh (visibility / new searcher) vs. flush (durability / Lucene commit) vs. merge
(segment cleanup,
MergePolicy/MergeScheduler). - Explain how a write replicates: document replication (
TransportReplicationAction) vs. segment replication (SegmentReplicationTargetService/SourceService). - Describe sequence-number tracking:
LocalCheckpointTracker, global checkpoint viaReplicationTracker. - Implement an
AnalysisPluginexposing a custom token filter and prove it with_analyze. - Explain how a mapping field type selects its analyzer and Lucene field.
Self-check questions
- What guarantees does the translog provide between two Lucene commits?
- Why is a freshly indexed document not searchable until a refresh?
- What is the difference, on the wire, between document replication and segment replication?
Rubric
| Criterion | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| Write path | Names classes | Walks happy path | Walks to Lucene | Walks edge cases |
| Refresh/flush/merge | Confused | Knows definitions | Knows triggers | Tunes/diagnoses |
| Replication | Aware | Knows doc-rep | Knows seg-rep | Reasons about seqno/checkpoints |
| Analysis/mapping | Aware | Reads a mapper | Builds a filter | Builds + tests an AnalysisPlugin |
| Engine debugging | Reads stack | Maps to source | Breakpoints in engine | Writes a repro test |
Pass threshold: 15/20.
Move to Level 7 when: you can set a breakpoint in IndexShard.applyIndexOperationOnPrimary, step
into InternalEngine.index, and explain refresh vs. flush vs. merge without notes.
M7 — Search and Aggregations (end of Week 14)
You can read the search fan-out, the per-shard phases, and the coordinating-node reduce.
Skills
- Walk search end to end:
TransportSearchActionfan-out → per-shardSearchService→QueryPhase→FetchPhase→ reduce inSearchPhaseController. - Explain the optional
DfsPhase(global term statistics) and when it matters. - Explain the aggregation lifecycle:
AggregatorFactory→Aggregator→InternalAggregation.reduce(...). - Explain why aggregations read DocValues, not the inverted index, and what
doc_count_error_upper_boundmeans. - Read a BM25
_explaintree and account for each term. - Build a custom aggregation and register it via
SearchPlugin.
Self-check questions
- Where does shard-local partial work become a globally correct answer in both search and aggs?
- What does
SearchContext/DefaultSearchContexthold per shard, and when is it released? - Why can a
termsaggregation be approximate across shards?
Rubric
| Criterion | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| Search path | Names phases | Orders them | Cites files | Walks with breakpoints |
| Reduce | Vague | Knows it exists | Knows it merges shards | Reasons about correctness |
| Aggregations | Aware | Knows factory→agg | Knows reduce | Builds a custom agg |
| DocValues lens | Confused | Knows aggs use them | Knows why | Reasons about cardinality/memory |
| QueryBuilders | Aware | Reads one | Knows toQuery path | Adds/extends a query |
Pass threshold: 15/20.
Move to Level 8 when: you can trace a _search from fan-out to reduce with breakpoints and you
have a custom aggregation registered via SearchPlugin that returns correct results.
M8 — Production Diagnostics and Real Contribution (end of Week 15)
You can reproduce a real GitHub issue, localize the root cause, and prepare a fix.
Skills
- Reproduce a reported issue locally with a failing test or a
curlrepro. - Read a stack trace and walk it into
server/to a file:method. - Use cluster diagnostics:
_cluster/health,_cat/shards,_cluster/allocation/explain,_nodes/stats, and the circuit-breaker stats. - Distinguish a core bug from a plugin/Dashboards/Lucene bug (correct attribution).
- Write a minimal regression test that fails before the fix and passes after.
- Open a PR with a DCO sign-off, a CHANGELOG entry, and a filled-out PR template, linked to the issue.
Self-check questions
- Given a wrong dashboard chart, how do you decide whether the bug is in Dashboards, core search, a plugin, or Lucene?
- What is the minimum a good issue reproduction contains?
- Why does a regression test belong in the same PR as the fix?
Rubric
| Criterion | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| Reproduction | None | Manual curl | Scripted | Added as a failing test |
| Root cause | Speculative | Localized | Cited file:method | Explained in the issue |
| Diagnostics | Guesses | Reads _cat/health | Uses allocation/explain | Cross-checks stats+logs |
| Attribution | Confused | Knows boundaries | Picks the right repo | Files/triages correctly |
| PR readiness | None | Draft | Signed + CHANGELOG | Template + linked issue |
Pass threshold: 16/20.
Move to the capstone when: you have a reproducible failing case for a real issue, a root cause localized to a file:method, and a regression test that gates the fix.
M9 — Capstone: You Have Shipped a Patch (end of Week 16)
You have taken a real OpenSearch issue through the full contribution cycle.
Skills
- Selected an appropriate, unassigned, in-scope issue.
- Reproduced and root-caused it.
- Implemented a minimal, idiomatic fix with a regression test.
- Submitted a PR in OpenSearch's accepted format: signed commits, CHANGELOG entry, PR template,
linked issue;
./gradlew precommitand module tests green. - Responded to at least one round of review feedback (real or simulated against the PR quality checklist).
Self-check questions
- Is your change minimal and focused, or does it bundle unrelated cleanup?
- Did you consider wire/index backward compatibility and add a serialization/BWC test if needed?
- Can you summarize the root cause and the fix in three sentences for a reviewer?
Rubric (20 points)
| Criterion | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| Issue selection | Random | Scoped | Justified | Aligned to roadmap |
| Reproduction | None | Manual | Scripted | Added as a test |
| Root cause | Speculative | Localized | Cited | Explained on the issue/PR |
| Implementation | Compiles | Tests pass | Idiomatic | Minimal, focused, BWC-aware |
| Submission | None | Draft | Submitted (signed + CHANGELOG) | Reviewed, feedback addressed |
Pass threshold: 16/20, and the change must pass ./gradlew precommit plus the tests on the
affected module.
Global Rubric (maintainer-readiness)
Use this every quarter, regardless of level, to self-assess against where OpenSearch maintainers operate.
| Dimension | 1 (Beginner) | 2 (Apprentice) | 3 (Practitioner) | 4 (Maintainer-ready) |
|---|---|---|---|---|
| Code | Reads org.opensearch.* | Modifies safely | Designs a subsystem change | Reviews others' PRs |
| Testing | Runs tests | Adds unit/integ tests | Writes regression + BWC suites | Drives test-infra / flaky triage |
| Distributed reasoning | Single node | Shards/replicas | Coordination + allocation | Reasons about consensus/recovery edge cases |
| Contribution & community | Opens issues | Opens PRs with sign-off + CHANGELOG | Reviews, attributes cross-repo bugs | Shapes RFCs, mentors, weighs release impact |
A maintainer-track contributor should be at level 3 on all four dimensions and level 4 on at least
one. Aim for 3/3/3/3 → 4/3/3/4 by month 12 of focused, sustained contribution. Maintainership in
OpenSearch is earned through demonstrated judgment over time — not PR volume — and the per-repo
MAINTAINERS.md reflects that.
Use these milestones together with the 16-week plan. When the calendar and the milestone disagree, the milestone wins: repeat the week until you pass the gate.