From assistant
Audits Armeria-based Java projects for event loop blocking issues—discovers patterns, scans operations, traces calls, produces fix plans without code changes. For latency spikes or pre-release checks.
npx claudepluginhub line/armeria --plugin assistantThis skill uses the workspace's default tool permissions.
A systematic, multi-phase audit that discovers how a project uses Armeria, scans for all categories of event loop blocking, traces call chains to determine thread context, and produces a structured report with fix plans. **No code is modified** — only analysis and planning.
Performs 12-Factor App compliance analysis on any codebase. Use when evaluating application architecture, auditing SaaS applications, or reviewing cloud-native applications.
Detects performance bottlenecks in CPU, memory, I/O, database, and lock contention layers. Provides analysis and remediation strategies for slow applications.
Reviews code for security vulnerabilities, performance issues, reliability, and quality using AI tools and static analyzers like Semgrep, SonarQube, CodeQL. Ideal for proactive code quality assurance.
Share bugs, ideas, or general feedback.
A systematic, multi-phase audit that discovers how a project uses Armeria, scans for all categories of event loop blocking, traces call chains to determine thread context, and produces a structured report with fix plans. No code is modified — only analysis and planning.
This skill is framework-generic — it works on any Armeria-based Java project by discovering the project's patterns dynamically, not assuming specific class names or directory structures.
For a comprehensive guide on Armeria's thread pools, executor selection, and programmatic usage patterns, see armeria-thread-pool-guide.md.
The Golden Rule: If Thread.currentThread() instanceof NonBlocking — you MUST NOT block. Armeria's event loop threads implement the NonBlocking marker interface. Any blocking operation on these threads stalls ALL connections sharing that event loop, causing cascading latency and potential deadlocks.
Before searching for problems, discover HOW the project uses Armeria. Launch an Explore agent (or do it yourself) with these specific searches. Record findings as they'll inform all subsequent phases.
Grep: Server\.builder\(\)|ServerBuilder
Scope: **/*.java
Read each match and document:
workerGroup(n), serviceWorkerGroup(n))blockingTaskExecutor(...)).decorate(...) calls)EventLoopGroup creationGrep: implements HttpService|extends AbstractHttpService|extends SimpleDecoratingHttpService
Scope: **/*.java
These classes have serve() methods that run on the event loop. List all of them — they are the primary audit targets.
Grep: @Get\(|@Post\(|@Put\(|@Delete\(|@Head\(|@Patch\(|@Options\(
Scope: **/*.java
For each, check if the method or class has @Blocking. Methods without @Blocking run on the event loop.
Grep: extends \w+ImplBase|GrpcService\.builder\(\)
Scope: **/*.java
Check whether useBlockingTaskExecutor(true) is configured in the GrpcService.builder(). If not, all gRPC handler methods run on the event loop.
Grep: THttpService\.builder\(\)|THttpService\.of\(
Scope: **/*.java
Same check — look for useBlockingTaskExecutor(true).
Grep: SimpleDecoratingHttpService|DecoratingHttpServiceFunction|\.decorate\(
Scope: **/*.java
Every decorator's serve() method runs on the event loop. List them all.
Grep: blockingTaskExecutor\(\)|ctx\.eventLoop\(\)|Executors\.new|new ThreadPoolExecutor|new ForkJoinPool|ExecutorService
Scope: **/*.java
Document all custom thread pools and how they're used. These are the "safe zones" where blocking is OK.
Grep: supplyAsync|thenApplyAsync|thenComposeAsync|thenAcceptAsync|ctx\.blockingTaskExecutor|makeContextAware
Scope: **/*.java
Understand the project's conventions for offloading work from the event loop. Look for:
runBlocking(Supplier) method in a base handler)ctx.blockingTaskExecutor() with additional logic (cancellation, metrics, tracing)@Blocking)A wrapper is "safe" if it provably submits the lambda/callable to a non-event-loop executor before the lambda body executes. Read the wrapper's implementation to verify — don't trust method names alone.
Synthesize findings into a concise map:
Search the entire project (or scoped path) for each category of blocking pattern. Exclude test files (**/test/**, **/tests/**) and generated code (**/generated/**, **/target/**, **/build/**).
See detection-patterns.md for the complete search patterns and triage guidance for all 7 categories:
.join(), .get(), Thread.sleep(), .await(), .lock(), etc.synchronized, ReentrantLock, StampedLock on request paths.thenApply() without Async where completing thread is event loop@Blocking, missing useBlockingTaskExecutor(true), blocking cancel handlers, missing context propagationFor each finding from Phase 2, perform call chain analysis to determine the thread context.
Identify enclosing method — What method contains the potentially blocking call?
Find all callers — Grep for the method name across the project:
Grep: methodName\(
Scope: **/*.java
When handling overloaded methods (same name, different signatures), read each call site to confirm it calls the blocking variant. Also check superclass/interface hierarchies — a serve() override in a subclass is still called from the event loop if the parent is an HttpService.
Trace to entry point — Follow the call chain upward until you reach one of:
serve(), @Get/@Post handler, gRPC handler, decorator)blockingTaskExecutor().execute(...), supplyAsync(...), etc.)whenRequestCancelling, setOnCancelHandler).submit(this::methodName))Handling ambiguity: If a method is called from BOTH event loop paths AND thread pool paths, classify based on the worst case (event loop path). Note both paths in the finding.
Determine thread context — At the entry point:
@Blocking annotation? → SafeuseBlockingTaskExecutor(true)? → SafesupplyAsync(..., blockingExecutor) call? → SafeClassify severity using this decision tree:
Is the blocking call provably inside a blocking executor or thread pool?
├── YES → SAFE (exclude from report or list separately)
├── NO, it's provably on the event loop:
│ ├── Is it an I/O operation, .join(), .get() without timeout, or Thread.sleep()?
│ │ └── YES → CRITICAL
│ ├── Is it .get(timeout, unit) or .await(timeout, unit)?
│ │ └── YES → HIGH (blocks event loop for the timeout duration)
│ ├── Is it a synchronized block or lock?
│ │ ├── Can it contend with a thread that holds the lock for >1ms?
│ │ │ └── YES → HIGH
│ │ └── Low contention, very fast critical section?
│ │ └── LOW
│ └── Is it a very fast operation (System.getProperty, in-memory map lookup)?
│ └── LOW
└── UNDETERMINED (e.g., CF continuation where completing thread varies):
├── Could the continuation body block?
│ ├── YES → MEDIUM (must verify at runtime or refactor to be safe)
│ └── NO (continuation is pure computation) → LOW
└── Is the source future always completed by a thread pool?
└── YES → SAFE
Key distinction: MEDIUM means "we cannot statically prove the thread context" — it requires either runtime verification (add logging/assertion) or defensive refactoring (use *Async variant with explicit executor).
For each non-SAFE finding, recommend one of the fix approaches documented in fix-templates.md:
| Template | When to use |
|---|---|
| FP-1 | Replace .join()/.get() with async chaining |
| FP-2 | Offload blocking work to ctx.blockingTaskExecutor() |
| FP-3 | Add @Blocking annotation to annotated service methods |
| FP-4 | Enable useBlockingTaskExecutor(true) for gRPC/Thrift services |
| FP-5 | Use *Async CF variant with explicit executor |
| FP-6 | Add timeout to unavoidable blocking |
| FP-7 | Make thread pool initialization thread-safe |
| FP-8 | Wire context propagation with makeContextAware |
Document the recommendation — do not apply it.
## Finding [ID]: [Short descriptive title]
**Severity**: CRITICAL / HIGH / MEDIUM / LOW
**File**: [relative path]:[line number]
**Thread context**: Event loop / Blocking executor / Undetermined
**Detection category**: [1-7 with category name]
### Description
[What the code does and why it's a blocking risk on the event loop]
### Call chain
[Armeria entry point (event loop)] → [Method A] → [Method B] → [blocking call at line X]
### Recommended fix
[Reference FP-1 through FP-8, with specific application to this case:
which method to change, what the new code should look like, which executor to use]
### Risk assessment
[What could break when applying the fix, edge cases to consider, tests to run]
At the end of all findings, produce a summary:
| # | Severity | File | Line | Category | Blocking Pattern | Fix |
|---|----------|------|------|----------|-----------------|-----|
| 1 | CRITICAL | FooService.java | 42 | 1 - Direct blocking | `.join()` | FP-1 |
| 2 | HIGH | BarHandler.java | 88 | 2 - I/O | FileInputStream | FP-2 |
| ... | ... | ... | ... | ... | ... | ... |
- Total findings: [N]
- CRITICAL: [N] | HIGH: [N] | MEDIUM: [N] | LOW: [N] | SAFE (excluded): [N]
- By category: Cat.1: [N] | Cat.2: [N] | Cat.3: [N] | Cat.4: [N] | Cat.5: [N] | Cat.6: [N] | Cat.7: [N]
Always include these recommendations regardless of findings:
log.*() calls-Dcom.linecorp.armeria.reportBlockedEventLoop=true
Follow this checklist to ensure nothing is skipped. Check off each item as you complete it.
HttpService implementations@Blocking presence/absenceuseBlockingTaskExecutor presence/absence.join() calls are inside thread pool code and are perfectly safe..thenApply() without Async is the most subtle source of event loop blocking. The continuation runs on whatever thread completes the source future.synchronized is always bad — It's only a problem if (a) it's on a request path reachable from the event loop AND (b) the lock can be contended by another thread holding it for a long time.serve() method that calls helperMethod() which calls utilMethod() which calls .join() is still blocking the event loop.log.info() on the event loop is a blocking I/O operation. This is a configuration issue, not a code issue, but still critical.