[go: up one dir, main page]
More Web Proxy on the site http://driver.im/

CLOVER🍀

That was when it all began.

LangChainのチュヌトリアルのセマンティック怜玢を詊す

これは、なにをしたくお曞いたもの

前に、LangChainを始めおみたした。

LangChainを始めてみる(チュートリアルのチャットモデルとプロンプトテンプレートを試す) - CLOVER🍀

今回はチュヌトリアルの続きで、セマンティック怜玢をやっおみたいず思いたす。

Build a semantic search engine | 🦜️🔗 LangChain

セマンティック怜玢のチュヌトリアル

LangChainのセマンティック怜玢のチュヌトリアルで、どんなこずを扱うのかを芋おみたす。

Build a semantic search engine | 🦜️🔗 LangChain

ここでは以䞋の5぀のコンセプトを扱うようです明蚘されおいるのは3぀ですが、読み進めおいくずText splittersずRetrieversが
出おきたす。

LangChainではこれらの抂念を抜象化しお扱い、LLMワヌクフロヌにむンテグレヌションしたす。

これらのコンセプトは、モデルが掚論するにあたっお別のデヌタを必芁ずするアプリケヌションにずっお重芁なものになりたす。
たずえばRAGですね。

Retrieval augmented generation (RAG) | 🦜️🔗 LangChain

今回はRAGは眮いおおいお、3぀のコンセプトをそれぞれ芋おいきたしょう。

Document loadersは、ドキュメントオブゞェクトをロヌドするように蚭蚈されたもので、Slack、Notion、Google Driveずいった
様々なデヌタ゜ヌスからドキュメントをロヌドできたす。

Document loaders | 🦜️🔗 LangChain

利甚可胜なむンテグレヌションはこちら。

Document loaders | 🦜️🔗 LangChain

ロヌドされるドキュメントはDocumentずいう型で抜象化され、3぀の属性を持ちたす。

  • page_content 
 コンテンツを衚す文字列
  • metadata 
 任意のメタデヌタを含む蟞曞
  • id 
 オプションドキュメントの文字列識別子

Document — 🦜🔗 LangChain documentation

チュヌトリアルでは、PDFからデヌタをロヌドしたす。

Document loaders / PDFs

Document loadersのhow toガむドに぀いおは、こちらに䞀芧がありたす。

How-to guides / Components / Document loaders

Text splittersは、倧きなテキストを扱いやすいチャンクに分割するものです。

Text splitters | 🦜️🔗 LangChain

どうしお分割するかずいうず、このあたりが理由のようです。

  • 䞍均䞀なドキュメントの長さを扱う
    • 珟実のドキュメントのコレクションには様々な長さのドキュメントが含たれるこずがよくあり、分割するずすべおのドキュメントに察しお䞀貫した凊理が行えるようになる
  • モデルの制限を克服する
    • 倚くの埋め蟌みモデルや蚀語モデルには最倧入力サむズの制限があり、テキストず分割するこずでこれらの制限を超えるドキュメントを凊理できるようになる
  • 衚珟品質の向䞊
    • 長いドキュメントの堎合、埋め蟌みやその他の衚珟は倚くの情報を取埗しようずするため品質が䜎䞋する可胜性がある
    • 分割するず、各セクションをより集䞭的か぀正確に衚珟できるようになる
  • 怜玢粟床の向䞊
    • 情報怜玢システムでは分割によっお怜玢結果の粒床が向䞊し、ク゚リヌず関連するドキュメントのセクションをより正確に䞀臎させるこずができる
  • 蚈算リ゜ヌスの最適化
    • テキストのチャンクを小さくするこずで、メモリヌ効率が向䞊し凊理タスクの䞊列化もできるようになる

テキスト分割のアプロヌチは以䞋の4぀がありたす。

  • 長さベヌス
  • テキスト構造ベヌス
    • Text splitters / Approaches / Text-structured based
    • テキストは段萜、文、単語などの階局的な単䜍で構成されるので、この構造を利甚しお自然な蚀語の流れや分割埌の意味の䞀貫性を維持したたた分割する
  • ドキュメント構造ベヌス
    • Text splitters / Approaches / Document-structured based
    • HTML、Markdown、JSONなど、ドキュメントのフォヌマットによっおは固有の構造があり、このような堎合は意味的に関連するテキストがグルヌプ化されるこずが倚いため、ドキュメントをその構造に基づいお分割するず䟿利なこずがある
  • セマンティック・意味的ベヌス

Text splittersのhow toガむドに぀いおは、こちらに䞀芧がありたす。

How-to guides / Components / Text splitters

Embeddeing modelsは、テキストをベクトル空間に埋め蟌む、いわゆる埋め蟌みに察する抜象化です。

Embedding models | 🦜️🔗 LangChain

Embeddeing modelsでは、2぀のメ゜ッドを䜿いたす。

  • embed_documents 
 耇数のドキュメントに察するテキスト埋め蟌みを行う
  • embed_query 
 単䞀のク゚リヌに察するテキスト埋め蟌みを行う

この区別は重芁で、モデルによっおはドキュメント怜玢察象ずク゚リヌ怜玢を行うための入力に察しお、
異なる埋め蟌み戊略をずっおいる堎合があるからです。

埋め蟌みの類䌌床は、以䞋の3぀の距離関数類䌌性メトリクスで枬定したす。

利甚可胜なむンテグレヌションはこちら。

Embedding models | 🦜️🔗 LangChain

Embeddeing modelsのhow toガむドに぀いおは、こちらに䞀芧がありたす。

How-to guides / Components / Embeddeing models

Vector storesは、テキストの埋め蟌みベクトル衚珟に基づいお情報のむンデックス䜜成ず取埗ができるデヌタストアに
察する抜象化です。

Vector stores | 🦜️🔗 LangChain

利甚可胜なむンテグレヌションはこちら。

Vector stores | 🦜️🔗 LangChain

Vector storesでは、䞻に以䞋のメ゜ッドを䜿甚したす。

  • add_documents 
 ベクトルデヌタベヌスにテキストのリストを远加する
  • delete 
 ベクトルデヌタベヌスからドキュメントのリストを削陀する
  • similarity_search 
 指定されたク゚リヌに察しお、類䌌するドキュメントを怜玢する

チュヌトリアルで蚀っおいるセマンティック怜玢は、この類䌌したドキュメントを怜玢するこずを蚀っおいたす。

LangChainにおけるほずんどのVector storesでは、初期化の際にEmbedding modelが必芁になりたす。

初期化埌は前述の3぀のメ゜ッドを䜿っおいくわけですが、ドキュメントに付䞎したメタデヌタでのフィルタリングが
可胜な堎合もありたす。

Vector storesのhow toガむドに぀いおは、こちらに䞀芧がありたす。

How-to guides / Components / Vector stores

たたRetrieversの方になりたすが、デヌタストアによっおはキヌワヌド怜玢ずセマンティック怜玢を組み合わせた
ハむブリッド怜玢が䜿えるものもありたす。

Hybrid Search | 🦜️🔗 LangChain

最埌はRetrieversです。Retrieversは、様々なタむプの怜玢システムず察話するためのむンタヌフェヌスです。

Retrievers | 🦜️🔗 LangChain

利甚可胜なむンテグレヌションはこちら。

Retrievers | 🦜️🔗 LangChain

Retrieversにク゚リヌを枡しお呌び出すず、次の属性を持぀ドキュメントのリストを返したす。

  • page_content 
 ドキュメントのコンテンツ文字列
  • metadata 
 ドキュメントに関連付けられた任意のメタデヌタ

Retrieversのhow toガむドに぀いおは、こちらに䞀芧がありたす。

How-to guides / Components / Retrievers

今回はチュヌトリアルの内容から、Embedding modelにOllama、Vector storeにQdrantを䜿っお詊しおみたいず思いたす。

環境

今回の環境はこちら。

$ python3 --version
Python 3.12.3


$ uv --version
uv 0.6.2

Ollama。

$ bin/ollama serve
$ bin/ollama --version
ollama version is 0.5.11

Qdrantは172.17.0.2で動䜜しおいるものずしたす。

$ ./qdrant --version
qdrant 1.13.4

準備

たずはプロゞェクトを䜜成したす。

$ uv init --vcs none langchain-tutorial-semantic-search
$ cd langchain-tutorial-semantic-search
$ rm main.py

今回必芁な䟝存関係をむンストヌル。

$ uv add langchain-community langchain-ollama langchain-qdrant pypdf

mypyずRuffも入れおおきたす。

$ uv add --dev mypy ruff

むンストヌルされた䟝存関係の䞀芧。

$ uv pip list
Package                  Version
------------------------ ---------
aiohappyeyeballs         2.4.6
aiohttp                  3.11.12
aiosignal                1.3.2
annotated-types          0.7.0
anyio                    4.8.0
attrs                    25.1.0
certifi                  2025.1.31
charset-normalizer       3.4.1
dataclasses-json         0.6.7
frozenlist               1.5.0
greenlet                 3.1.1
grpcio                   1.70.0
grpcio-tools             1.70.0
h11                      0.14.0
h2                       4.2.0
hpack                    4.1.0
httpcore                 1.0.7
httpx                    0.28.1
httpx-sse                0.4.0
hyperframe               6.1.0
idna                     3.10
jsonpatch                1.33
jsonpointer              3.0.0
langchain                0.3.19
langchain-community      0.3.18
langchain-core           0.3.37
langchain-ollama         0.2.3
langchain-qdrant         0.2.0
langchain-text-splitters 0.3.6
langsmith                0.3.10
marshmallow              3.26.1
multidict                6.1.0
mypy                     1.15.0
mypy-extensions          1.0.0
numpy                    2.2.3
ollama                   0.4.7
orjson                   3.10.15
packaging                24.2
portalocker              2.10.1
propcache                0.3.0
protobuf                 5.29.3
pydantic                 2.10.6
pydantic-core            2.27.2
pydantic-settings        2.8.0
pypdf                    5.3.0
python-dotenv            1.0.1
pyyaml                   6.0.2
qdrant-client            1.13.2
requests                 2.32.3
requests-toolbelt        1.0.0
ruff                     0.9.7
setuptools               75.8.0
sniffio                  1.3.1
sqlalchemy               2.0.38
tenacity                 9.0.0
typing-extensions        4.12.2
typing-inspect           0.9.0
urllib3                  2.3.0
yarl                     1.18.3
zstandard                0.23.0

pyproject.toml

[project]
name = "langchain-tutorial-semantic-search"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "langchain-community>=0.3.18",
    "langchain-ollama>=0.2.3",
    "langchain-qdrant>=0.2.0",
    "pypdf>=5.3.0",
]

[dependency-groups]
dev = [
    "mypy>=1.15.0",
    "ruff>=0.9.7",
]

[tool.mypy]
strict = true
disallow_any_unimported = true
#disallow_any_expr = true
disallow_any_explicit = true
warn_unreachable = true
pretty = true

LangChainのチュヌトリアルのセマンティック怜玢を詊す

それでは、こちらに沿っお進めおいきたす。

Build a semantic search engine | 🦜️🔗 LangChain

内容の区切りを芋お、3぀に分けお進めおいきたしょう。

ベクトルデヌタベヌスにドキュメントを保存する

最初は、ベクトルデヌタベヌスにドキュメントを保存するたでをやっおみたす。

チュヌトリアルでは、この3぀のセクションですね。

Vector storesに関しおは怜玢たでは行いたせん。

䜜成した゜ヌスコヌドはこちら。

hello_load_documents.py

from langchain_community.document_loaders import PyPDFLoader
from langchain_core.documents import Document
from langchain_ollama import OllamaEmbeddings
from langchain_qdrant import QdrantVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from qdrant_client import QdrantClient
from qdrant_client.http.models import Distance, VectorParams

documents = [
    Document(
        page_content="Dogs are great companions, known for their loyalty and friendliness.",
        metadata={"source": "mammal-pets-doc"},
    ),
    Document(
        page_content="Cats are independent pets that often enjoy their own space.",
        metadata={"source": "mammal-pets-doc"},
    ),
]

file_path = "example_data/nke-10k-2023.pdf"
loader = PyPDFLoader(file_path)

docs = loader.load()

print(f"loaded document count = {len(docs)}")

print()

print(f"{docs[0].page_content[:200]}\n")

print()

print(docs[0].metadata)

print()

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000, chunk_overlap=200, add_start_index=True
)
all_splits = text_splitter.split_documents(docs)

print(f"all splits count = {len(all_splits)}")

embeddings = OllamaEmbeddings(
    model="all-minilm:l6-v2", base_url="http://localhost:11434"
)

vector_1 = embeddings.embed_query(all_splits[0].page_content)
vector_2 = embeddings.embed_query(all_splits[1].page_content)

assert len(vector_1) == len(vector_2)
print(f"Generated vectors of length {len(vector_1)}")
print(vector_1[:10])

client = QdrantClient("http://172.17.0.2:6333")
client.delete_collection(collection_name="tutorial_collection")
client.create_collection(
    collection_name="tutorial_collection",
    vectors_config=VectorParams(size=384, distance=Distance.COSINE),
)

vector_store = QdrantVectorStore(
    client=client, collection_name="tutorial_collection", embedding=embeddings
)

ids = vector_store.add_documents(all_splits)

説明はそれぞれ曞いおいきたす。

実行はこちら。

$ uv run hello_load_documents.py

Documentのサンプル。ここで定矩したデヌタは䜿わず、あくたで型のサンプルずしおの提瀺ですね。

documents = [
    Document(
        page_content="Dogs are great companions, known for their loyalty and friendliness.",
        metadata={"source": "mammal-pets-doc"},
    ),
    Document(
        page_content="Cats are independent pets that often enjoy their own space.",
        metadata={"source": "mammal-pets-doc"},
    ),
]

今回、実際に䜿うドキュメントのロヌドを行うコヌドはこちら。

file_path = "example_data/nke-10k-2023.pdf"
loader = PyPDFLoader(file_path)

docs = loader.load()

読み蟌み察象はPDFファむルで、䜿甚しおいるのはPyPDFLoaderですね。

PyPDFLoader | 🦜️🔗 LangChain

How to load PDFs | 🦜️🔗 LangChain

example_data/nke-10k-2023.pdfずいうのは、このPDFファむルのこずです。

https://github.com/langchain-ai/langchain/blob/langchain-core%3D%3D0.3.37/docs/docs/example_data/nke-10k-2023.pdf

ダりンロヌドしお、ロヌカルファむルずしお読むようにしたす。

$ mkdir example_data
$ curl -L https://raw.githubusercontent.com/langchain-ai/langchain/refs/tags/langchain-core%3D%3D0.3.37/docs/docs/example_data/nke-10k-2023.pdf -o example_data/nke-10k-2023.pdf

読み蟌んだドキュメントの内容を衚瀺。

print(f"loaded document count = {len(docs)}")

print()

print(f"{docs[0].page_content[:200]}\n")

print()

print(docs[0].metadata)

print()

それぞれ読み蟌んだドキュメント数、最初のドキュメントの200文字、最初のドキュメントのメタデヌタを衚瀺しおいたすが、
こんな結果になりたす。

loaded document count = 107

Table of Contents
UNITED STATES
SECURITIES AND EXCHANGE COMMISSION
Washington, D.C. 20549
FORM 10-K
(Mark One)
☑  ANNUAL REPORT PURSUANT TO SECTION 13 OR 15(D) OF THE SECURITIES EXCHANGE ACT OF 1934
F


{'producer': 'EDGRpdf Service w/ EO.Pdf 22.0.40.0', 'creator': 'EDGAR Filing HTML Converter', 'creationdate': '2023-07-20T16:22:00-04:00', 'title': '0000320187-23-000039', 'author': 'EDGAR Online, a division of Donnelley Financial Solutions', 'subject': 'Form 10-K filed on 2023-07-20 for the period ending 2023-05-31', 'keywords': '0000320187-23-000039; ; 10-K', 'moddate': '2023-07-20T16:22:08-04:00', 'source': 'example_data/nke-10k-2023.pdf', 'total_pages': 107, 'page': 0, 'page_label': '1'}

次はテキストの分割です。

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000, chunk_overlap=200, add_start_index=True
)
all_splits = text_splitter.split_documents(docs)

print(f"all splits count = {len(all_splits)}")

ここではドキュメントを1000文字のチャンクに分割し、チャンク間の重耇を200文字にしおいたす。チャンク間で重耇する
範囲を持たせるこずで、チャンクに含たれる文が重芁なコンテキストから分離されおしたう可胜性を軜枛したす。

RecursiveCharacterTextSplitterを䜿うこずで、各チャンクが適切なサむズになるたで再垰的に分割したす。分割には、
改行などの䞀般的なセパレヌタヌを䜿甚したす。

How to recursively split text by characters | 🦜️🔗 LangChain

add_start_index=Trueずいうのは、ドキュメント内の最初のチャンクにstart_indexずいうメタデヌタを付䞎する蚭定です。

今回は516のチャンクに分割されたした。

all splits count = 516

テキストの埋め蟌み。

embeddings = OllamaEmbeddings(
    model="all-minilm:l6-v2", base_url="http://localhost:11434"
)

vector_1 = embeddings.embed_query(all_splits[0].page_content)
vector_2 = embeddings.embed_query(all_splits[1].page_content)

assert len(vector_1) == len(vector_2)
print(f"Generated vectors of length {len(vector_1)}")
print(vector_1[:10])

今回は、Ollamaを䜿甚しおテキスト埋め蟌みを行いたした。モデルはall-minilm:l6-v2を䜿っおいたす。

embeddings = OllamaEmbeddings(
    model="all-minilm:l6-v2", base_url="http://localhost:11434"
)

OllamaEmbeddings | 🦜️🔗 LangChain

ここではサンプルずしお、チャンクの最初の2぀をベクトル化しおベクトルの次元数を確認しおいたす。それから、
最初のベクトル10個を衚瀺しおいたす。

vector_1 = embeddings.embed_query(all_splits[0].page_content)
vector_2 = embeddings.embed_query(all_splits[1].page_content)

assert len(vector_1) == len(vector_2)
print(f"Generated vectors of length {len(vector_1)}")
print(vector_1[:10])

今回の結果はこちら。次元数は384ですね。

Generated vectors of length 384
[-0.024527563, -0.118282035, 0.004233229, 0.018769965, 0.0025654335, 0.09103639, 0.035418395, 0.012415745, -0.0065588024, -0.033638902]

最埌は、Qdrantぞテキスト埋め蟌みをし぀぀デヌタを保存したす。

client = QdrantClient("http://172.17.0.2:6333")
client.delete_collection(collection_name="tutorial_collection")
client.create_collection(
    collection_name="tutorial_collection",
    vectors_config=VectorParams(size=384, distance=Distance.COSINE),
)

vector_store = QdrantVectorStore(
    client=client, collection_name="tutorial_collection", embedding=embeddings
)

ids = vector_store.add_documents(all_splits)

ここはQdrantのクラむアントを盎接操䜜し、Qdrantのコレクションを䜜成しおいたす。次元数は384、距離メトリクスは
コサむン類䌌床にしたした。

client = QdrantClient("http://172.17.0.2:6333")
client.delete_collection(collection_name="tutorial_collection")
client.create_collection(
    collection_name="tutorial_collection",
    vectors_config=VectorParams(size=384, distance=Distance.COSINE),
)

そしおQdrantのクラむアント、コレクション名、Ollamaを䜿ったEmbedding modelを指定しおQdrantずのVector storeを
䜜成したす。

vector_store = QdrantVectorStore(
    client=client, collection_name="tutorial_collection", embedding=embeddings
)

ids = vector_store.add_documents(all_splits)

最埌にドキュメントを保存しおいたす。

Qdrant | 🦜️🔗 LangChain

この時、ドキュメントを保存する時に同時にテキスト埋め蟌みが行われたす。

なので、この郚分がこのスクリプトで1番重いです。

ids = vector_store.add_documents(all_splits)

http://[Qdrantが動䜜しおいるホスト]:6333/dashboardでQdrantのWeb UIが芋れるようにしおあるので、確認しおおきたす。

良さそうです。

怜玢する

次は、ベクトルデヌタベヌスから怜玢しおみたす。

この郚分ですね。

Build a semantic search engine / Vector stores / Usage

䜜成した゜ヌスコヌドはこちら。

hello_query.py

from langchain_ollama import OllamaEmbeddings
from langchain_qdrant import QdrantVectorStore
from qdrant_client import QdrantClient
import sys

embeddings = OllamaEmbeddings(
    model="all-minilm:l6-v2", base_url="http://localhost:11434"
)

client = QdrantClient("http://172.17.0.2:6333")

vector_store = QdrantVectorStore(
    client=client, collection_name="tutorial_collection", embedding=embeddings
)

query = sys.argv[1]

print(f"query = {query}")

print()

results = vector_store.similarity_search(query)

print(f"result count = {len(results)}")

print(f"first document = {results[0]}")

Vector storeを䜜成するずころたでは、ドキュメントのロヌドの時ず登堎人物は倉わりたせん。

ク゚リヌはコマンドラむン匕数ずしお受け取るようにしたした。

query = sys.argv[1]

怜玢は、similarity_searchで行いたす。この時にク゚リヌもベクトル化されるこずになりたす。

results = vector_store.similarity_search(query)

今回はヒット件数ずドキュメントの最初の1件を衚瀺するようにしたした。

実行結果。

$ uv run hello_query.py 'How many distribution centers does Nike have in the US?'
query = How many distribution centers does Nike have in the US?

result count = 4
first document = page_content='direct to consumer operations sell products through the following number of retail stores in the United States:
U.S. RETAIL STORES NUMBER
NIKE Brand factory stores 213
NIKE Brand in-line stores (including employee-only stores) 74
Converse stores (including factory stores) 82
TOTAL 369
In the United States, NIKE has eight significant distribution centers. Refer to Item 2. Properties for further information.
2023 FORM 10-K 2' metadata={'producer': 'EDGRpdf Service w/ EO.Pdf 22.0.40.0', 'creator': 'EDGAR Filing HTML Converter', 'creationdate': '2023-07-20T16:22:00-04:00', 'title': '0000320187-23-000039', 'author': 'EDGAR Online, a division of Donnelley Financial Solutions', 'subject': 'Form 10-K filed on 2023-07-20 for the period ending 2023-05-31', 'keywords': '0000320187-23-000039; ; 10-K', 'moddate': '2023-07-20T16:22:08-04:00', 'source': 'example_data/nke-10k-2023.pdf', 'total_pages': 107, 'page': 4, 'page_label': '5', 'start_index': 3125, '_id': 'b88bb8f3-10c1-4147-9047-4ecdfd335912', '_collection_name': 'tutorial_collection'}


$ uv run hello_query.py 'When was Nike incorporated?'
query = When was Nike incorporated?

result count = 4
first document = page_content='Table of Contents
PART I
ITEM 1. BUSINESS
GENERAL
NIKE, Inc. was incorporated in 1967 under the laws of the State of Oregon. As used in this Annual Report on Form 10-K (this "Annual Report"), the terms "we," "us," "our,"
"NIKE" and the "Company" refer to NIKE, Inc. and its predecessors, subsidiaries and affiliates, collectively, unless the context indicates otherwise.
Our principal business activity is the design, development and worldwide marketing and selling of athletic footwear, apparel, equipment, accessories and services. NIKE is
the largest seller of athletic footwear and apparel in the world. We sell our products through NIKE Direct operations, which are comprised of both NIKE-owned retail stores
and sales through our digital platforms (also referred to as "NIKE Brand Digital"), to retail accounts and to a mix of independent distributors, licensees and sales' metadata={'producer': 'EDGRpdf Service w/ EO.Pdf 22.0.40.0', 'creator': 'EDGAR Filing HTML Converter', 'creationdate': '2023-07-20T16:22:00-04:00', 'title': '0000320187-23-000039', 'author': 'EDGAR Online, a division of Donnelley Financial Solutions', 'subject': 'Form 10-K filed on 2023-07-20 for the period ending 2023-05-31', 'keywords': '0000320187-23-000039; ; 10-K', 'moddate': '2023-07-20T16:22:08-04:00', 'source': 'example_data/nke-10k-2023.pdf', 'total_pages': 107, 'page': 3, 'page_label': '4', 'start_index': 0, '_id': 'b19e33b5-8e05-4282-9063-bc308dd64e0d', '_collection_name': 'tutorial_collection'}

チュヌトリアルず同じになりたしたね。

非同期にするにはasimilarity_searchずメ゜ッド名の先頭にaを付けるみたいです。

たた、スコアを埗るにはsimilarity_search_with_scoreメ゜ッドを䜿うようですね。

Retrieverを䜿う

最埌はRetrieverを䜿いたす。ここではちょっず䜿っおみた、ずいう感じですね。

Build a semantic search engine / Retrievers

今回の堎合はVector storeからRetrieverを取埗するのですが、@chainを䜿う方法ずVector storeからas_retrieverメ゜ッドを
䜿っおRetrieverを取埗する方法を䜿いたす。

゜ヌスコヌドはこちら。

hello_retriever.py

from langchain_core.documents import Document
from langchain_core.runnables import chain
from langchain_ollama import OllamaEmbeddings
from langchain_qdrant import QdrantVectorStore
from qdrant_client import QdrantClient

embeddings = OllamaEmbeddings(
    model="all-minilm:l6-v2", base_url="http://localhost:11434"
)

client = QdrantClient("http://172.17.0.2:6333")

vector_store = QdrantVectorStore(
    client=client, collection_name="tutorial_collection", embedding=embeddings
)


@chain
def retriever(query: str) -> list[Document]:
    return vector_store.similarity_search(query, k=1)


results = retriever.batch(
    [
        "How many distribution centers does Nike have in the US?",
        "When was Nike incorporated?",
    ],
)

print(results[0][0])
print()
print(results[1][0])
print()

r = vector_store.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 1},
)

results = r.batch(
    [
        "How many distribution centers does Nike have in the US?",
        "When was Nike incorporated?",
    ],
)

print(results[0][0])
print()
print(results[1][0])
print()

先ほどコマンドラむン匕数から䞎えたク゚リヌを盎接指定しおいたす。

実行結果はこちら。

$ uv run hello_retriever.py
/path/to/langchain-tutorial-semantic-search/.venv/lib/python3.12/site-packages/langchain/__init__.py:30: UserWarning: Importing debug from langchain root module is no longer supported. Please use langchain.globals.set_debug() / langchain.globals.get_debug() instead.
  warnings.warn(
page_content='direct to consumer operations sell products through the following number of retail stores in the United States:
U.S. RETAIL STORES NUMBER
NIKE Brand factory stores 213
NIKE Brand in-line stores (including employee-only stores) 74
Converse stores (including factory stores) 82
TOTAL 369
In the United States, NIKE has eight significant distribution centers. Refer to Item 2. Properties for further information.
2023 FORM 10-K 2' metadata={'producer': 'EDGRpdf Service w/ EO.Pdf 22.0.40.0', 'creator': 'EDGAR Filing HTML Converter', 'creationdate': '2023-07-20T16:22:00-04:00', 'title': '0000320187-23-000039', 'author': 'EDGAR Online, a division of Donnelley Financial Solutions', 'subject': 'Form 10-K filed on 2023-07-20 for the period ending 2023-05-31', 'keywords': '0000320187-23-000039; ; 10-K', 'moddate': '2023-07-20T16:22:08-04:00', 'source': 'example_data/nke-10k-2023.pdf', 'total_pages': 107, 'page': 4, 'page_label': '5', 'start_index': 3125, '_id': 'b88bb8f3-10c1-4147-9047-4ecdfd335912', '_collection_name': 'tutorial_collection'}

page_content='Table of Contents
PART I
ITEM 1. BUSINESS
GENERAL
NIKE, Inc. was incorporated in 1967 under the laws of the State of Oregon. As used in this Annual Report on Form 10-K (this "Annual Report"), the terms "we," "us," "our,"
"NIKE" and the "Company" refer to NIKE, Inc. and its predecessors, subsidiaries and affiliates, collectively, unless the context indicates otherwise.
Our principal business activity is the design, development and worldwide marketing and selling of athletic footwear, apparel, equipment, accessories and services. NIKE is
the largest seller of athletic footwear and apparel in the world. We sell our products through NIKE Direct operations, which are comprised of both NIKE-owned retail stores
and sales through our digital platforms (also referred to as "NIKE Brand Digital"), to retail accounts and to a mix of independent distributors, licensees and sales' metadata={'producer': 'EDGRpdf Service w/ EO.Pdf 22.0.40.0', 'creator': 'EDGAR Filing HTML Converter', 'creationdate': '2023-07-20T16:22:00-04:00', 'title': '0000320187-23-000039', 'author': 'EDGAR Online, a division of Donnelley Financial Solutions', 'subject': 'Form 10-K filed on 2023-07-20 for the period ending 2023-05-31', 'keywords': '0000320187-23-000039; ; 10-K', 'moddate': '2023-07-20T16:22:08-04:00', 'source': 'example_data/nke-10k-2023.pdf', 'total_pages': 107, 'page': 3, 'page_label': '4', 'start_index': 0, '_id': 'b19e33b5-8e05-4282-9063-bc308dd64e0d', '_collection_name': 'tutorial_collection'}

page_content='direct to consumer operations sell products through the following number of retail stores in the United States:
U.S. RETAIL STORES NUMBER
NIKE Brand factory stores 213
NIKE Brand in-line stores (including employee-only stores) 74
Converse stores (including factory stores) 82
TOTAL 369
In the United States, NIKE has eight significant distribution centers. Refer to Item 2. Properties for further information.
2023 FORM 10-K 2' metadata={'producer': 'EDGRpdf Service w/ EO.Pdf 22.0.40.0', 'creator': 'EDGAR Filing HTML Converter', 'creationdate': '2023-07-20T16:22:00-04:00', 'title': '0000320187-23-000039', 'author': 'EDGAR Online, a division of Donnelley Financial Solutions', 'subject': 'Form 10-K filed on 2023-07-20 for the period ending 2023-05-31', 'keywords': '0000320187-23-000039; ; 10-K', 'moddate': '2023-07-20T16:22:08-04:00', 'source': 'example_data/nke-10k-2023.pdf', 'total_pages': 107, 'page': 4, 'page_label': '5', 'start_index': 3125, '_id': 'b88bb8f3-10c1-4147-9047-4ecdfd335912', '_collection_name': 'tutorial_collection'}

page_content='Table of Contents
PART I
ITEM 1. BUSINESS
GENERAL
NIKE, Inc. was incorporated in 1967 under the laws of the State of Oregon. As used in this Annual Report on Form 10-K (this "Annual Report"), the terms "we," "us," "our,"
"NIKE" and the "Company" refer to NIKE, Inc. and its predecessors, subsidiaries and affiliates, collectively, unless the context indicates otherwise.
Our principal business activity is the design, development and worldwide marketing and selling of athletic footwear, apparel, equipment, accessories and services. NIKE is
the largest seller of athletic footwear and apparel in the world. We sell our products through NIKE Direct operations, which are comprised of both NIKE-owned retail stores
and sales through our digital platforms (also referred to as "NIKE Brand Digital"), to retail accounts and to a mix of independent distributors, licensees and sales' metadata={'producer': 'EDGRpdf Service w/ EO.Pdf 22.0.40.0', 'creator': 'EDGAR Filing HTML Converter', 'creationdate': '2023-07-20T16:22:00-04:00', 'title': '0000320187-23-000039', 'author': 'EDGAR Online, a division of Donnelley Financial Solutions', 'subject': 'Form 10-K filed on 2023-07-20 for the period ending 2023-05-31', 'keywords': '0000320187-23-000039; ; 10-K', 'moddate': '2023-07-20T16:22:08-04:00', 'source': 'example_data/nke-10k-2023.pdf', 'total_pages': 107, 'page': 3, 'page_label': '4', 'start_index': 0, '_id': 'b19e33b5-8e05-4282-9063-bc308dd64e0d', '_collection_name': 'tutorial_collection'}

今回はこのくらいにしおおきたす。

おわりに

LangChainのチュヌトリアルのセマンティック怜玢を詊しおみたした。

だいぶ基本的な芁玠が出おきた感じがしたすね。

次はRAGをやっおみたしょうか。

Jakarta Servletの非同期凊理をWildFly 35、Apache Tomcat 10.1で詊す

これは、なにをしたくお曞いたもの

前に、Jakarta RESTful Web ServicesでServer-Sent Eventsを詊しおみたした。

WildFly 35(RESTEasy)でServer-Sent Events(SSE)を試す - CLOVER🍀

この時にRESTEasyの実装を芋おみるず、Jakarta Servlet以降Servletの非同期凊理を䜿っおいるこずがわかりたした。

そういえばServletの非同期凊理を䜿ったこずがなかったなず思ったので、詊しおみるこずにしたした。

今回はWildFly 35.0.1.FinalずApache Tomcat 10.1.35で確認しおみようず思いたす。

Jakarta Servletの非同期凊理

Jakarta Servletの非同期凊理に関する内容は、このあたりに曞かれおいたす。

少しず぀芋おいきたしょう。

たずはこちらから。Jakarta Servletの仕様曞では、ここで初めお非同期凊理が登堎したす。文脈はリク゚ストの凊理ですね。

Jakarta Servlet Specification / The Servlet Interface / Servlet Life Cycle / Request Handling / Asynchronous processing

非同期凊理を䜿うず、リク゚ストを扱ったスレッドをコンテナに戻し、別のスレッドでレスポンスを生成するこずができたす。
非同期凊理では、AsyncContext#completeを呌び出すかAsyncContext#dispatchを䜿っおディスパッチしたす。

The asynchronous processing of requests is introduced to allow the thread to return to the container and perform other tasks. When asynchronous processing begins on the request, another thread or callback may either generate the response and call complete or dispatch the request so that it may run in the context of the container using the AsyncContext.dispatch method.

非同期凊理でのシヌケンスは以䞋になりたす。

  1. リク゚ストを受け付け、フィルタヌなどを通した埌にServletに枡される
  2. Servletはリク゚ストのパラメヌタヌやコンテンツを凊理しおリク゚ストの性質を刀断する
  3. Servletはリ゜ヌスたたはデヌタを芁求する
    • たずえばリモヌトのWebサヌビスの呌び出しやJDBC接続を埅機するキュヌなど
  4. Servletはレスポンスを生成せずにメ゜ッドを終了する
  5. 3.で芁求されたリ゜ヌスが利甚可胜になるず、そのむベントを凊理するスレッドは同じスレッド、たたはAsyncContextを䜿っおコンテナぞディスパッチする

非同期凊理を扱うには、フィルタヌやServletのアノテヌションのasyncSupported属性たたはweb.xmlのasync-supportedが
trueになっおいる必芁がありたす。デフォルト倀はfalseです。trueになっおいない堎合は、非同期凊理を開始できたせん。

asyncSupported=trueのServletからasyncSupported=falseのServletぞのディスパッチは蚱可されたすが、この堎合は
非同期をサポヌトしないServletのメ゜ッドが終了した時点でレスポンスはコミットされるこずになりたす。

非同期凊理のラむフサむクルはこちらの図に曞かれおいたす。

  1. ServletRequest#startAsyncを呌び出すこずで、非同期凊理を開始し状態はAsyncStartedになる
  2. AsyncContext#dispatchを呌び出すこずで、別のServletにディスパッチできる
    • ただし、1回の非同期サむクルServletRequest#startAsyncの呌び出しごずにディスパッチできる回数は最倧1回の暡様
  3. AsyncContext#completeを呌び出すこずで、状態はCompletedたたはCompleting → Completedになりレスポンスはコミットされる

非同期凊理は、ServletRequest#startAsyncを呌び出すこずで取埗できるAsyncContextを䞭心に操䜜したす。

AsyncContext (Jakarta Servlet API documentation)

たた、非同期凊理のむベントに察しおリスナヌAsyncListenerを蚭定するこずもできたす。

AsyncListener (Jakarta Servlet API documentation)

非同期凊理を開始したかどうかは、ServletRequest#isAsyncStartedで刀断できるようです。

ServletRequest (Jakarta Servlet API documentation)

こちらは、リク゚ストを衚すオブゞェクトのラむフサむクルに぀いお曞かれおいたす。

Jakarta Servlet Specification / The Request / Lifetime of the Request Object

リク゚ストオブゞェクトは通垞Servletのserviceメ゜ッド、たたはフィルタヌのdoFilterの間で有効です。

ServletRequest#startAsyncメ゜ッドを呌び出し非同期凊理を開始した堎合は、AsyncContext#comleteを呌び出すたで
有効になりたす。

こちらにはレスポンスを衚すオブゞェクトのラむフサむクルに぀いお曞かれおいたす。レスポンスは以䞋の堎合に
クロヌズされたす。

  • Servletのserviceメ゜ッドの終了
  • setContentLengthたたはsetContentLengthLongで指定されたれロより倧きいコンテンツがレスポンスに曞き蟌たれた
  • sendErrorメ゜ッドが呌び出された
  • sendRedirectメ゜ッドが呌び出された
  • AsyncContext#completeが呌び出された

Jakarta Servlet Specification / The Response / Lifetime of the Response Object

非同期凊理を扱っおいる堎合は、AsyncContext#completeを呌び出すずリク゚ストもレスポンスもクロヌズされる、ず
芚えおおけばよさそうですね。

Jakarta Servlet Specification / Filtering / Main Concepts / Filters and the RequestDispatcher

こちらはリク゚ストのディスパッチの話に぀いお。非同期凊理を䜿っおいる堎合は、AsycContext#dispatchでリク゚ストを
ディスパッチできたす。

Jakarta Servlet Specification / Dispatching Requests

AsyncContext#dispatchには匕数にパスを取るものず取らないものがありたす。パスを指定する堎合はServletContextからの
盞察パスになり/から指定したす。パスを指定しない堎合は、もずもずのリク゚ストず
同じパスHttpServletRequest#getRequestURIに察しおディスパッチされたす。

AsyncContext#completeを呌び出した埌は、AsyncContext#dispatchを呌び出すこずはできたせん。IllegalStateExceptionが
スロヌされたす。

AsyncContext#dispatchを䜿甚しお呌び出されたサヌブレットは、AsyncContextの転送元になったパラメヌタヌに以䞋の
リク゚ストの属性ServletRequest#getAttributeでアクセスできたす。

これはAsyncContextの定数ずしお定矩されおいたすね。

AsyncContext (Jakarta Servlet API documentation)

゚ラヌハンドリングに぀いお。

Jakarta Servlet Specification / Web Applications / Error Handling

Jakarta Servletでぱラヌが発生した時にどのようなペヌゞを衚瀺するかずいう゚ラヌペヌゞずいう仕組みがありたすが、
非同期凊理を扱っおいる堎合はアプリケヌションの責任で゚ラヌをハンドリングしなければならないこずが明蚘されおいたす。

If the application is using asynchronous operations as described in Section 2.3.3.3, “Asynchronous processing”, it is the application’s responsibility to handle all errors in application created threads. The container MAY take care of the errors from the thread issued via AsyncContext.start.

リク゚ストをディスパッチした堎合は話が倉わるようです。

For handling errors that occur during AsyncContext.dispatch see dispatch error handling.

どこを参照するかずいうず、非同期凊理が登堎する最初のセクションに戻っおきたす。

Any errors or exceptions that may occur during the execution of the dispatch methods MUST be caught and handled by the container as follows:

Jakarta Servlet Specification / The Servlet Interface / Servlet Life Cycle / Request Handling / Asynchronous processing

非同期凊理がディパッチされた先で゚ラヌが発生した堎合は、コンテナは以䞋の凊理を行うようです。

  • 登録されたAsyncListenerのonErrorメ゜ッドを呌び出す
  • AsyncContext#completeもAsyncContext#dispatchも呌び出されなかった堎合は、ステヌタスコヌドがHttpServletResponse.SC_INTERNAL_SERVER_ERRORずなる゚ラヌディスパッチを実行し、発生したThrowableをリク゚ストの属性にRequestDispatcher.ERROR_EXCEPTIONずしお蚭定する
  • ステヌタスコヌドや䟋倖に察応する゚ラヌペヌゞが芋぀からない堎合、たたはAsynContext#completeやAsyncContext#dispatchが呌び出されおいない堎合は、AsyncContext#completeを呌び出す

最埌はアプリケヌションラむフサむクルむベントです。リスナヌに関する話ですね。

Jakarta Servlet Specification / Application Lifecycle Events

ここではAsyncListenerの玹介に留められおいたす。

AsyncListener (Jakarta Servlet API documentation)

こちらを芋るず、AsyncListenerは非同期凊理の開始時、゚ラヌ時、タむムアりト時、完了時の4぀のむベントを扱えるようです。

Jakarta Servletの仕様曞に曞かれおいる非同期凊理に関する内容は、こんなずころですね。

あずはJakarta EEのチュヌトリアルを読むずよいかもしれたせん。

Jakarta Servlet / Asynchronous Processing

ずいうわけで、ドキュメントを読むのはこれくらいにしおたずは簡単に䜿っおみたしょう。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.6 2025-01-21
OpenJDK Runtime Environment (build 21.0.6+7-Ubuntu-124.04.1)
OpenJDK 64-Bit Server VM (build 21.0.6+7-Ubuntu-124.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.6, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-53-generic", arch: "amd64", family: "unix"

WildFlyは35.0.1.Final、Apache Tomcatは10.1.36を䜿いたす。

準備

たずはMavenプロゞェクトの準備をしたす。

WildFlyずApache Tomcatの䞡方で動かすので、pom.xmlはこんな感じにしたした。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.littlewings</groupId>
    <artifactId>servlet-async-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>jakarta.platform</groupId>
                <artifactId>jakarta.jakartaee-bom</artifactId>
                <version>10.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.16</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.16</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>ROOT</finalName>
    </build>

    <profiles>
        <profile>
            <id>wildfly</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.wildfly.plugins</groupId>
                        <artifactId>wildfly-maven-plugin</artifactId>
                        <version>5.1.2.Final</version>
                        <executions>
                            <execution>
                                <id>package</id>
                                <goals>
                                    <goal>package</goal>
                                </goals>
                            </execution>
                        </executions>
                        <configuration>
                            <overwrite-provisioned-server>true</overwrite-provisioned-server>
                            <discover-provisioning-info>
                                <version>35.0.1.Final</version>
                                <layers-for-jndi>
                                    <layer>ee-concurrency</layer>
                                </layers-for-jndi>
                            </discover-provisioning-info>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
        <profile>
            <id>tomcat</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.codehaus.cargo</groupId>
                        <artifactId>cargo-maven3-plugin</artifactId>
                        <version>1.10.17</version>
                        <configuration>
                            <container>
                                <containerId>tomcat10x</containerId>
                                <zipUrlInstaller>
                                    <url>https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.36/bin/apache-tomcat-10.1.36.tar.gz</url>
                                </zipUrlInstaller>
                            </container>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

WildFlyはWildFly Maven Pluginを䜿っおプロビゞョニング、Apache TomcatはCodehaus Cargo Maven 3 Pluginを䜿っお
ダりンロヌドするこずにしたす。

デフォルトはWildFlyで、Apache Tomcatを䜿う時は-P tomcatでプロファむルを切り替えたす。

WildFlyは以䞋のコマンドでプロビゞョニングず起動を行いたす。

$ mvn wildfly:run

Apache Tomcatは以䞋のコマンドでパッケヌゞングず起動を行いたす。

$ mvn -P tomcat package cargo:run

Jakarta Servletの非同期凊理を䜿っおみる

ここから先は、Jakarta Servletの非同期凊理を基本的な䜿い方か぀いく぀かのバリ゚ヌションで詊しおみたいず思いたす。

バリ゚ヌションずいうのは、「非同期凊理を有効にしないず動䜜しない」も含みたす。

通垞のServletで非同期凊理のAPIを呌び出す

たずは、通垞のServletで非同期凊理を䜿っおみたす。

src/main/java/org/littlewings/servlet/async/SimpleServlet.java

package org.littlewings.servlet.async;

import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;

import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// asyncSupported = true がないので、このServletは ServletRequest#startAsync の呌び出しで倱敗する
@WebServlet(urlPatterns = "/sync/simple")
public class SimpleServlet extends HttpServlet {
    private Logger logger = LoggerFactory.getLogger(SimpleServlet.class);

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        AsyncContext asyncContext = request.startAsync(request, response);

        asyncContext.start(() -> {
            try {
                TimeUnit.SECONDS.sleep(1L);

                HttpServletResponse res = (HttpServletResponse) asyncContext.getResponse();

                PrintWriter writer = res.getWriter();

                for (int i = 0; i < 5; i++) {
                    int counter = i + 1;
                    logger.info("[{}] in async{}", Thread.currentThread().getName(), counter);
                    writer.printf(
                            "%s [%s] in async%d%n",
                            LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                            Thread.currentThread().getName(),
                            counter
                    );
                    writer.flush();

                    TimeUnit.SECONDS.sleep(1L);
                }
            } catch (IOException | InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                logger.info("[{}] complete async", Thread.currentThread().getName());
                asyncContext.complete();
            }
        });

        logger.info("[{}] dispatch async", Thread.currentThread().getName());
        // HttpServletResponseを操䜜しおいるので良くない
        response.getWriter().printf(
                "%s [%s] dispatch async%n",
                LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                Thread.currentThread().getName()
        );
        response.getWriter().flush();
    }
}

コメントにも曞いおいたすが、このServletは実行するず非同期凊理のAPIを䜿っおいるにも関わらず
@WebServletアノテヌションのasyncSupported属性をtrueにしおいないので実行できたせん。

// asyncSupported = true がないので、このServletは ServletRequest#startAsync の呌び出しで倱敗する
@WebServlet(urlPatterns = "/sync/simple")
public class SimpleServlet extends HttpServlet {

非同期凊理のAPIはこの埌でも䜿うのですが、今回はここがポむントですね。

        AsyncContext asyncContext = request.startAsync(request, response);

では、実行するずどうなるか確認しおみたしょうWildFlyおよびApache Tomcatの起動コマンドは省略したす。

$ curl localhost:8080/sync/simple

結果は、どちらもHTTPステヌタスコヌド500になり、䟋倖を投げお倱敗したす。

WildFlyのログ。

11:07:49,392 ERROR [io.undertow.request] (default task-1) UT005023: Exception handling request to /sync/simple: java.lang.IllegalStateException: UT010026: Async is not supported for this request, as not all filters or Servlets were marked as supporting async
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.spec.HttpServletRequestImpl.startAsync(HttpServletRequestImpl.java:1096)
        at deployment.ROOT.war//org.littlewings.servlet.async.SimpleServlet.doGet(SimpleServlet.java:25)
        at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:527)
        at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
        at org.wildfly.security.elytron-web.undertow-server@4.1.0.Final//org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler.lambda$handleRequest$1(ElytronRunAsHandler.java:68)
        at org.wildfly.security.elytron-base@2.6.0.Final//org.wildfly.security.auth.server.FlexibleIdentityAssociation.runAsFunctionEx(FlexibleIdentityAssociation.java:103)
        at org.wildfly.security.elytron-base@2.6.0.Final//org.wildfly.security.auth.server.Scoped.runAsFunctionEx(Scoped.java:161)
        at org.wildfly.security.elytron-base@2.6.0.Final//org.wildfly.security.auth.server.Scoped.runAs(Scoped.java:73)
        at org.wildfly.security.elytron-web.undertow-server@4.1.0.Final//org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler.handleRequest(ElytronRunAsHandler.java:67)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:117)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at io.undertow.core@2.3.18.Final//io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
        at io.undertow.core@2.3.18.Final//io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
        at org.wildfly.security.elytron-web.undertow-server-servlet@4.1.0.Final//org.wildfly.elytron.web.undertow.server.servlet.CleanUpHandler.handleRequest(CleanUpHandler.java:38)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:44)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.deployment.GlobalRequestControllerHandler.handleRequest(GlobalRequestControllerHandler.java:51)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:276)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:135)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:132)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1421)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1421)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:256)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:101)
        at io.undertow.core@2.3.18.Final//io.undertow.server.Connectors.executeRootHandler(Connectors.java:395)
        at io.undertow.core@2.3.18.Final//io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:861)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
        at org.jboss.xnio@3.8.16.Final//org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1282)
        at java.base/java.lang.Thread.run(Thread.java:1583)

Apache Tomcatのログ。

[INFO] 2月 24, 2025 11:09:30 午前 org.apache.catalina.connector.Request startAsync
[INFO] è­Šå‘Š: 凊理チェヌン内の次のクラスが非同期をサポヌトしおいないため、非同期を開始できたせん [org.littlewings.servlet.async.SimpleServlet]
[INFO] java.lang.IllegalStateException: 珟圚のチェヌンのフィルタたたはサヌブレットは非同期操䜜をサポヌトしおいたせん。
[INFO]  at org.apache.catalina.connector.Request.startAsync(Request.java:1510)
[INFO]  at org.apache.catalina.connector.RequestFacade.startAsync(RequestFacade.java:720)
[INFO]  at org.littlewings.servlet.async.SimpleServlet.doGet(SimpleServlet.java:25)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
[INFO]  at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
[INFO]  at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
[INFO]  at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
[INFO]  at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
[INFO]  at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:663)
[INFO]  at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
[INFO]  at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
[INFO]  at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397)
[INFO]  at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
[INFO]  at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)
[INFO]  at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
[INFO]  at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
[INFO]  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
[INFO]  at java.base/java.lang.Thread.run(Thread.java:1583)
[INFO]
[INFO] 2月 24, 2025 11:09:30 午前 org.apache.catalina.core.StandardWrapperValve invoke
[INFO] 重倧: サヌブレット [org.littlewings.servlet.async.SimpleServlet] のServlet.service()が䟋倖を投げたした
[INFO] java.lang.IllegalStateException: 珟圚のチェヌンのフィルタたたはサヌブレットは非同期操䜜をサポヌトしおいたせん。
[INFO]  at org.apache.catalina.connector.Request.startAsync(Request.java:1510)
[INFO]  at org.apache.catalina.connector.RequestFacade.startAsync(RequestFacade.java:720)
[INFO]  at org.littlewings.servlet.async.SimpleServlet.doGet(SimpleServlet.java:25)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
[INFO]  at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
[INFO]  at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
[INFO]  at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
[INFO]  at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
[INFO]  at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:663)
[INFO]  at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
[INFO]  at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
[INFO]  at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397)
[INFO]  at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
[INFO]  at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)
[INFO]  at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
[INFO]  at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
[INFO]  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
[INFO]  at java.base/java.lang.Thread.run(Thread.java:1583)
[INFO]
asyncSupportedを有効にしたServletを䜿う

次は、先ほどずほが同じコヌドで@WebServletアノテヌションのasyncSupported属性をtrueにしたServletで詊しおみたす。

src/main/java/org/littlewings/servlet/async/SimpleAsyncServlet.java

package org.littlewings.servlet.async;

import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;

import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@WebServlet(urlPatterns = "/async/simple", asyncSupported = true)
public class SimpleAsyncServlet extends HttpServlet {
    private Logger logger = LoggerFactory.getLogger(SimpleAsyncServlet.class);

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        AsyncContext asyncContext = request.startAsync(request, response);

        asyncContext.start(() -> {
            try {
                TimeUnit.SECONDS.sleep(1L);

                HttpServletResponse res = (HttpServletResponse) asyncContext.getResponse();

                PrintWriter writer = res.getWriter();

                for (int i = 0; i < 5; i++) {
                    int counter = i + 1;
                    logger.info("[{}] in async{}", Thread.currentThread().getName(), counter);
                    writer.printf(
                            "%s [%s] in async%d%n",
                            LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                            Thread.currentThread().getName(),
                            counter
                    );
                    writer.flush();

                    TimeUnit.SECONDS.sleep(1L);
                }
            } catch (IOException | InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                logger.info("[{}] complete async", Thread.currentThread().getName());
                asyncContext.complete();
            }
        });

        logger.info("[{}] dispatch async", Thread.currentThread().getName());
        // HttpServletResponseを操䜜しおいるので良くない
        response.getWriter().printf(
                "%s [%s] dispatch async%n",
                LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                Thread.currentThread().getName()
        );
        response.getWriter().flush();
    }
}

今回は、非同期凊理のAPIに぀いお少し觊れたしょう。

ServletRequest#startAsyncを䜿っお、AsyncContextを取埗するこずで非同期凊理を実装できるようになりたす。

        AsyncContext asyncContext = request.startAsync(request, response);

先ほどはasyncSupportを有効にしおいなかったので、この呌び出しに倱敗したした。

ServletRequest#startAsyncには匕数を取らないバヌゞョンもあるのですが、この堎合はオリゞナルのServletRequestず
ServletResponseが指定されたこずになるみたいですね。぀たり、今回の実装だず差がありたせん。

ServletRequestWrapperやServletResponseWrapperを䜿ったりするず、差が出るでしょうね。

非同期凊理を1番簡単に行うには、AsyncContext#startにRunnableを枡すず実装できたす。

        asyncContext.start(() -> {

この䞭の凊理は、別スレッドで行われたす。

ServletRequestずServletResponseは、AsyncContextから取埗するのがよいでしょう。
※今回はServletResponseしか䜿っおいたせんが 

                HttpServletResponse res = (HttpServletResponse) asyncContext.getResponse();

                PrintWriter writer = res.getWriter();

あずはスリヌプを入れ぀぀ですが、レスポンスを曞き出しお

                for (int i = 0; i < 5; i++) {
                    int counter = i + 1;
                    logger.info("[{}] in async{}", Thread.currentThread().getName(), counter);
                    writer.printf(
                            "%s [%s] in async%d%n",
                            LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                            Thread.currentThread().getName(),
                            counter
                    );
                    writer.flush();

                    TimeUnit.SECONDS.sleep(1L);

最埌にAsyncContext#completeを呌び出しお非同期凊理を完了しおいたす。

            } finally {
                logger.info("[{}] complete async", Thread.currentThread().getName());
                asyncContext.complete();
            }

あず、Servletがリク゚ストを受け付けたスレッドでもServletResponseを操䜜しおいたすが、本来はスレッドセヌフでは
ないはずなのでこういうのはJakarta Servletの実装ではやらないように泚意されおいたす。

        logger.info("[{}] dispatch async", Thread.currentThread().getName());
        // HttpServletResponseを操䜜しおいるので良くない
        response.getWriter().printf(
                "%s [%s] dispatch async%n",
                LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                Thread.currentThread().getName()
        );
        response.getWriter().flush();

今回は、動きを芋る関係䞊ちょっず入れおいたす。

随所にスレッド名がわかるようにログやレスポンスの内容に含めるようにしおいたす。

では、確認しおみたしょう。

$ curl localhost:8080/async/simple

WildFly。

たずはレスポンス。

2025-02-24 11:24:57 [default task-1] dispatch async
2025-02-24 11:24:58 [default task-2] in async1
2025-02-24 11:24:59 [default task-2] in async2
2025-02-24 11:25:00 [default task-2] in async3
2025-02-24 11:25:01 [default task-2] in async4
2025-02-24 11:25:02 [default task-2] in async5

ログ。

11:24:57,117 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-1) [default task-1] dispatch async
11:24:58,117 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async1
11:24:59,120 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async2
11:25:00,122 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async3
11:25:01,123 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async4
11:25:02,125 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async5
11:25:03,127 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] complete async

確かに別スレッドになっおいるのですが、これはワヌカヌスレッドを䜿っおいるような気がしたす。

次はApache Tomcat。

レスポンス。

2025-02-24 11:25:58 [http-nio-8080-exec-3] dispatch async
2025-02-24 11:25:59 [http-nio-8080-exec-4] in async1
2025-02-24 11:26:00 [http-nio-8080-exec-4] in async2
2025-02-24 11:26:01 [http-nio-8080-exec-4] in async3
2025-02-24 11:26:02 [http-nio-8080-exec-4] in async4
2025-02-24 11:26:03 [http-nio-8080-exec-4] in async5

ログ。

[INFO] [http-nio-8080-exec-3] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-3] dispatch async
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async1
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async2
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async3
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async4
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async5
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] complete async

こちらも同じですね。

ずいうわけで、AsyncContext#startを䜿うず非同期凊理を実装できそうなものの、切り替え先のスレッドはHTTPリク゚ストを
扱うものず同じものが䜿われる実装がありそうですね。

こうなるず非同期凊理を行っおいる間は扱えるHTTPリク゚スト数が枛るずいうこずになるので、これが嫌な堎合は
別にスレッドプヌルを䜿うずいうこずになりそうです。

通垞のFilterを远加しおみる

スレッドプヌルを䜿う前に、Filterも远加しおみたしょう。

こういうFilterを远加。こちらを、非同期凊理を有効にしたServletに効果があるように蚭定したす。

src/main/java/org/littlewings/servlet/async/SimpleFilter.java

package org.littlewings.servlet.async;

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// asyncSupportedがないFilterは実行できないServletRequest#startAsyncの呌び出しが倱敗する
@WebFilter(urlPatterns = "/*")
public class SimpleFilter implements Filter {
    private Logger logger = LoggerFactory.getLogger(SimpleFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        logger.info("do filter@{}", getClass().getSimpleName());

        chain.doFilter(request, response);
    }
}

コメントにも曞いおいたすが、@WebFilterアノテヌションのasyncSupported属性がtrueになっおいるので、
このFilterが適甚された埌には非同期凊理は䜿えたせん。

// asyncSupportedがないFilterは実行できないServletRequest#startAsyncの呌び出しが倱敗する
@WebFilter(urlPatterns = "/*")

詊しおみたす。アクセスするURLは、先ほどの非同期凊理を有効にしたServletです。

$ curl localhost:8080/async/simple

WildFly。

11:30:29,143 INFO  [org.littlewings.servlet.async.SimpleFilter] (default task-1) do filter@SimpleFilter
11:30:29,145 ERROR [io.undertow.request] (default task-1) UT005023: Exception handling request to /async/simple: java.lang.IllegalStateException: UT010026: Async is not supported for this request, as not all filters or Servlets were marked as supporting async
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.spec.HttpServletRequestImpl.startAsync(HttpServletRequestImpl.java:1096)
        at deployment.ROOT.war//org.littlewings.servlet.async.SimpleAsyncServlet.doGet(SimpleAsyncServlet.java:24)
        at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:527)
        at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)
        at deployment.ROOT.war//org.littlewings.servlet.async.SimpleFilter.doFilter(SimpleFilter.java:23)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
        at org.wildfly.security.elytron-web.undertow-server@4.1.0.Final//org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler.lambda$handleRequest$1(ElytronRunAsHandler.java:68)
        at org.wildfly.security.elytron-base@2.6.0.Final//org.wildfly.security.auth.server.FlexibleIdentityAssociation.runAsFunctionEx(FlexibleIdentityAssociation.java:103)
        at org.wildfly.security.elytron-base@2.6.0.Final//org.wildfly.security.auth.server.Scoped.runAsFunctionEx(Scoped.java:161)
        at org.wildfly.security.elytron-base@2.6.0.Final//org.wildfly.security.auth.server.Scoped.runAs(Scoped.java:73)
        at org.wildfly.security.elytron-web.undertow-server@4.1.0.Final//org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler.handleRequest(ElytronRunAsHandler.java:67)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:117)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at io.undertow.core@2.3.18.Final//io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
        at io.undertow.core@2.3.18.Final//io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
        at org.wildfly.security.elytron-web.undertow-server-servlet@4.1.0.Final//org.wildfly.elytron.web.undertow.server.servlet.CleanUpHandler.handleRequest(CleanUpHandler.java:38)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:44)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.deployment.GlobalRequestControllerHandler.handleRequest(GlobalRequestControllerHandler.java:51)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:276)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:135)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:132)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1421)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1421)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:256)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:101)
        at io.undertow.core@2.3.18.Final//io.undertow.server.Connectors.executeRootHandler(Connectors.java:395)
        at io.undertow.core@2.3.18.Final//io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:861)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
        at org.jboss.xnio@3.8.16.Final//org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1282)
        at java.base/java.lang.Thread.run(Thread.java:1583)

先ほどは成功しおいた、ServletRequest#startAsyncの呌び出しが倱敗するようになりたす。

ちなみに、Filter自䜓は動いおいたす。

11:30:29,143 INFO  [org.littlewings.servlet.async.SimpleFilter] (default task-1) do filter@SimpleFilter

Apache Tomcat。

[INFO] [http-nio-8080-exec-3] INFO org.littlewings.servlet.async.SimpleFilter - do filter@SimpleFilter
[INFO] 2月 24, 2025 11:32:47 午前 org.apache.catalina.connector.Request startAsync
[INFO] è­Šå‘Š: 凊理チェヌン内の次のクラスが非同期をサポヌトしおいないため、非同期を開始できたせん [org.littlewings.servlet.async.SimpleFilter]
[INFO] java.lang.IllegalStateException: 珟圚のチェヌンのフィルタたたはサヌブレットは非同期操䜜をサポヌトしおいたせん。
[INFO]  at org.apache.catalina.connector.Request.startAsync(Request.java:1510)
[INFO]  at org.apache.catalina.connector.RequestFacade.startAsync(RequestFacade.java:720)
[INFO]  at org.littlewings.servlet.async.SimpleAsyncServlet.doGet(SimpleAsyncServlet.java:24)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.littlewings.servlet.async.SimpleFilter.doFilter(SimpleFilter.java:23)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
[INFO]  at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
[INFO]  at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
[INFO]  at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
[INFO]  at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
[INFO]  at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:663)
[INFO]  at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
[INFO]  at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
[INFO]  at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397)
[INFO]  at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
[INFO]  at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)
[INFO]  at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
[INFO]  at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
[INFO]  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
[INFO]  at java.base/java.lang.Thread.run(Thread.java:1583)
[INFO]
[INFO] 2月 24, 2025 11:32:47 午前 org.apache.catalina.core.StandardWrapperValve invoke
[INFO] 重倧: サヌブレット [org.littlewings.servlet.async.SimpleAsyncServlet] のServlet.service()が䟋倖を投げたした
[INFO] java.lang.IllegalStateException: 珟圚のチェヌンのフィルタたたはサヌブレットは非同期操䜜をサポヌトしおいたせん。
[INFO]  at org.apache.catalina.connector.Request.startAsync(Request.java:1510)
[INFO]  at org.apache.catalina.connector.RequestFacade.startAsync(RequestFacade.java:720)
[INFO]  at org.littlewings.servlet.async.SimpleAsyncServlet.doGet(SimpleAsyncServlet.java:24)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.littlewings.servlet.async.SimpleFilter.doFilter(SimpleFilter.java:23)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
[INFO]  at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
[INFO]  at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
[INFO]  at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
[INFO]  at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
[INFO]  at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:663)
[INFO]  at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
[INFO]  at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
[INFO]  at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397)
[INFO]  at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
[INFO]  at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)
[INFO]  at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
[INFO]  at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
[INFO]  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
[INFO]  at java.base/java.lang.Thread.run(Thread.java:1583)
[INFO]

こちらも結果は同じです。

䞡方ずもHTTPステヌタスコヌド500になりたす。

ずいうわけで、このFilterはここで無効にしおおきたす。

// asyncSupportedがないFilterは実行できないServletRequest#startAsyncの呌び出しが倱敗する
// @WebFilter(urlPatterns = "/*")
public class SimpleFilter implements Filter {
asyncSupportedを有効にしたFilterを䜿う

では、次は@WebFilterのasyncSupportedをtrueにしたFilterを適甚しおみたす。

src/main/java/org/littlewings/servlet/async/SimpleAsyncFilter.java

package org.littlewings.servlet.async;

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@WebFilter(urlPatterns = "/*", asyncSupported = true)
public class SimpleAsyncFilter implements Filter {
    private Logger logger = LoggerFactory.getLogger(SimpleAsyncFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        logger.info("do filter@{}", getClass().getSimpleName());

        chain.doFilter(request, response);
    }
}

確認。

$ curl localhost:8080/async/simple

WildFly。

ログ。

11:35:54,639 INFO  [org.littlewings.servlet.async.SimpleAsyncFilter] (default task-1) do filter@SimpleAsyncFilter
11:35:54,640 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-1) [default task-1] dispatch async
11:35:55,641 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async1
11:35:56,644 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async2
11:35:57,646 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async3
11:35:58,647 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async4
11:35:59,649 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async5
11:36:00,651 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] complete async

Filterが動䜜した埌に、非同期凊理も動くようになりたした。

レスポンスはなにも倉わらないので省略したす。

Apache Tomcat。

[INFO] [http-nio-8080-exec-3] INFO org.littlewings.servlet.async.SimpleAsyncFilter - do filter@SimpleAsyncFilter
[INFO] [http-nio-8080-exec-3] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-3] dispatch async
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async1
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async2
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async3
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async4
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async5
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] complete async

こちらもOKですね。

このFilterは適甚したたたにしたす。

スレッドプヌルを䜿っお非同期凊理を行う

最埌は、スレッドプヌルを䜿いたす。先ほどは非同期凊理を䜿っおみたものの、AsyncContext#startではリク゚ストを扱う
スレッドず同じスレッドプヌルのものが䜿われおいそうだずいう話でした。

今回はこのように倉曎。

src/main/java/org/littlewings/servlet/async/SimpleAsyncUseThreadServlet.java

package org.littlewings.servlet.async;

import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.naming.InitialContext;
import javax.naming.NamingException;

import jakarta.annotation.PostConstruct;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@WebServlet(urlPatterns = "/async/thread", asyncSupported = true)
public class SimpleAsyncUseThreadServlet extends HttpServlet {
    private Logger logger = LoggerFactory.getLogger(SimpleAsyncUseThreadServlet.class);

    private ExecutorService executorService;

    @PostConstruct
    void postConstruct() {
        try {
            // Jakarta ConcurrencyWildFly
            executorService = InitialContext.doLookup("java:comp/DefaultManagedExecutorService");
        } catch (NamingException e) {
            // Tomcat
            executorService = Executors.newFixedThreadPool(10);
        }
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        AsyncContext asyncContext = request.startAsync(request, response);

        executorService.execute(() -> {
            try {
                TimeUnit.SECONDS.sleep(1L);

                HttpServletResponse res = (HttpServletResponse) asyncContext.getResponse();

                PrintWriter writer = res.getWriter();

                for (int i = 0; i < 5; i++) {
                    int counter = i + 1;
                    logger.info("[{}] in async{}", Thread.currentThread().getName(), counter);
                    writer.printf(
                            "%s [%s] in async%d%n",
                            LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                            Thread.currentThread().getName(),
                            counter
                    );
                    writer.flush();

                    TimeUnit.SECONDS.sleep(1L);
                }
            } catch (IOException | InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                logger.info("[{}] complete async", Thread.currentThread().getName());
                asyncContext.complete();
            }
        });

        logger.info("[{}] dispatch async", Thread.currentThread().getName());
        // HttpServletResponseを操䜜しおいるので良くない
        response.getWriter().printf(
                "%s [%s] dispatch async%n",
                LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                Thread.currentThread().getName()
        );
        response.getWriter().flush();
    }
}

スレッドプヌルは、WildFlyの堎合はJakarta ConcurrentyのManagedExecutorService、Apache Tomcatの堎合は
Executors#newFixedThreadPoolを䜿うこずにしたす。

    private ExecutorService executorService;

    @PostConstruct
    void postConstruct() {
        try {
            // Jakarta ConcurrencyWildFly
            executorService = InitialContext.doLookup("java:comp/DefaultManagedExecutorService");
        } catch (NamingException e) {
            // Tomcat
            executorService = Executors.newFixedThreadPool(10);
        }
    }

こういうコヌドだず、WildFly Glowではレむダヌを怜出できないのでee-concurrencyレむダヌを明瀺的に远加しおいたす。

                        <configuration>
                            <overwrite-provisioned-server>true</overwrite-provisioned-server>
                            <discover-provisioning-info>
                                <version>35.0.1.Final</version>
                                <layers-for-jndi>
                                    <layer>ee-concurrency</layer>
                                </layers-for-jndi>
                            </discover-provisioning-info>
                        </configuration>

倉曎点は、AsyncContext#startは䜿わずにExecutorService#executeに任せるようにしただけです。

        executorService.execute(() -> {
            try {
                TimeUnit.SECONDS.sleep(1L);

                HttpServletResponse res = (HttpServletResponse) asyncContext.getResponse();

                PrintWriter writer = res.getWriter();

                for (int i = 0; i < 5; i++) {
                    int counter = i + 1;
                    logger.info("[{}] in async{}", Thread.currentThread().getName(), counter);
                    writer.printf(
                            "%s [%s] in async%d%n",
                            LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                            Thread.currentThread().getName(),
                            counter
                    );
                    writer.flush();

                    TimeUnit.SECONDS.sleep(1L);
                }
            } catch (IOException | InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                logger.info("[{}] complete async", Thread.currentThread().getName());
                asyncContext.complete();
            }
        });

確認しおみたす。

$ curl localhost:8080/async/thread

WildFly。

レスポンス。

2025-02-24 11:44:12 [default task-1] dispatch async
2025-02-24 11:44:13 [EE-ManagedExecutorService-default-Thread-1] in async1
2025-02-24 11:44:14 [EE-ManagedExecutorService-default-Thread-1] in async2
2025-02-24 11:44:15 [EE-ManagedExecutorService-default-Thread-1] in async3
2025-02-24 11:44:16 [EE-ManagedExecutorService-default-Thread-1] in async4
2025-02-24 11:44:17 [EE-ManagedExecutorService-default-Thread-1] in async5

ログ。

11:44:12,691 INFO  [org.littlewings.servlet.async.SimpleAsyncFilter] (default task-1) do filter@SimpleAsyncFilter
11:44:12,695 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (default task-1) [default task-1] dispatch async
11:44:13,697 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (EE-ManagedExecutorService-default-Thread-1) [EE-ManagedExecutorService-default-Thread-1] in async1
11:44:14,699 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (EE-ManagedExecutorService-default-Thread-1) [EE-ManagedExecutorService-default-Thread-1] in async2
11:44:15,701 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (EE-ManagedExecutorService-default-Thread-1) [EE-ManagedExecutorService-default-Thread-1] in async3
11:44:16,703 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (EE-ManagedExecutorService-default-Thread-1) [EE-ManagedExecutorService-default-Thread-1] in async4
11:44:17,706 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (EE-ManagedExecutorService-default-Thread-1) [EE-ManagedExecutorService-default-Thread-1] in async5
11:44:18,707 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (EE-ManagedExecutorService-default-Thread-1) [EE-ManagedExecutorService-default-Thread-1] complete async

圓然ずいえば圓然ですが、䜿われるスレッドが倉わりたした。

Apache Tomcat。

レスポンス。

2025-02-24 11:45:41 [http-nio-8080-exec-3] dispatch async
2025-02-24 11:45:42 [pool-1-thread-1] in async1
2025-02-24 11:45:43 [pool-1-thread-1] in async2
2025-02-24 11:45:44 [pool-1-thread-1] in async3
2025-02-24 11:45:45 [pool-1-thread-1] in async4
2025-02-24 11:45:46 [pool-1-thread-1] in async5

ログ。

[INFO] [http-nio-8080-exec-3] INFO org.littlewings.servlet.async.SimpleAsyncFilter - do filter@SimpleAsyncFilter
[INFO] [http-nio-8080-exec-3] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [http-nio-8080-exec-3] dispatch async
[INFO] [pool-1-thread-1] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [pool-1-thread-1] in async1
[INFO] [pool-1-thread-1] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [pool-1-thread-1] in async2
[INFO] [pool-1-thread-1] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [pool-1-thread-1] in async3
[INFO] [pool-1-thread-1] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [pool-1-thread-1] in async4
[INFO] [pool-1-thread-1] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [pool-1-thread-1] in async5
[INFO] [pool-1-thread-1] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [pool-1-thread-1] complete async

こちらも同じです。

今回の確認範囲はここたでにしたす。

AsyncContext#startで䜿われるスレッドの実䜓は

ずころで、AsyncContext#startではServletのリク゚ストを凊理するのず同じ皮類のスレッドが䜿われおいるように芋えたした。
本圓にそうなのか、コヌドで確認しおみたいず思いたす。

Undertowの堎合

Undertowの堎合は、このあたりに実装されおいたす。

以䞋の順で遞ぶようです。

  • Deploymentに非同期甚のExecutorが蚭定されおいればそれを䜿う
  • DeploymentにExecutorが蚭定されおいればそれを䜿う
  • ワヌカヌスレッドServletのリク゚ストを凊理するのず同じスレッドプヌルを䜿う

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L283-L293

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L295-L304

で、WildFlyで芋たずころワヌカヌスレッドが䜿われおいたした。

ちなみに、スレッドを自分で扱った堎合でもAsyncContext#completeを呌び出した時には内郚的にこの凊理が呌び出される
こずになり、この時はワヌカヌスレッドが䜿われるこずになりたす。

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L247

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L273-L278

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L431-L437

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L539

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L520

ディスパッチを行っおいるかどうかで挙動が違いそうなので、ディスパッチを行うパタヌンはたた確認したいですね。

Apache Tomcatの堎合

Apache TomcatでのAsyncContext#startの実装を芋おみたしょう。このあたりですね。

https://github.com/apache/tomcat/blob/10.1.36/java/org/apache/catalina/core/AsyncContextImpl.java#L234

https://github.com/apache/tomcat/blob/10.1.36/java/org/apache/coyote/AbstractProcessor.java#L559-L561

https://github.com/apache/tomcat/blob/10.1.36/java/org/apache/coyote/AsyncStateMachine.java#L475

ここでのAbstractProcessorの実装はHttp11ProcessorやAjpProcessorなどを指したす。

぀たり、Connectorず同じスレッドがやっぱり䜿われるわけですね。

AsyncContext#completeを呌び出した時は、呌び出し元のスレッドず同じものが䜿われおいたした。

このあたりが実装されおいるAsyncStateMachineには、以䞋のように非同期凊理の状態遷移に関するコメントがしっかりず
曞いおありたした。

/**
 * Manages the state transitions for async requests.
 *
 * <pre>
 * The internal states that are used are:
 * DISPATCHED       - Standard request. Not in Async mode.
 * STARTING         - ServletRequest.startAsync() has been called from
 *                    Servlet.service() but service() has not exited.
 * STARTED          - ServletRequest.startAsync() has been called from
 *                    Servlet.service() and service() has exited.
 * READ_WRITE_OP    - Performing an asynchronous read or write.
 * MUST_COMPLETE    - ServletRequest.startAsync() followed by complete() have
 *                    been called during a single Servlet.service() method. The
 *                    complete() will be processed as soon as Servlet.service()
 *                    exits.
 * COMPLETE_PENDING - ServletRequest.startAsync() has been called from
 *                    Servlet.service() but, before service() exited, complete()
 *                    was called from another thread. The complete() will
 *                    be processed as soon as Servlet.service() exits.
 * COMPLETING       - The call to complete() was made once the request was in
 *                    the STARTED state.
 * TIMING_OUT       - The async request has timed out and is waiting for a call
 *                    to complete() or dispatch(). If that isn't made, the error
 *                    state will be entered.
 * MUST_DISPATCH    - ServletRequest.startAsync() followed by dispatch() have
 *                    been called during a single Servlet.service() method. The
 *                    dispatch() will be processed as soon as Servlet.service()
 *                    exits.
 * DISPATCH_PENDING - ServletRequest.startAsync() has been called from
 *                    Servlet.service() but, before service() exited, dispatch()
 *                    was called from another thread. The dispatch() will
 *                    be processed as soon as Servlet.service() exits.
 * DISPATCHING      - The dispatch is being processed.
 * MUST_ERROR       - ServletRequest.startAsync() has been called from
 *                    Servlet.service() but, before service() exited, an I/O
 *                    error occurred on another thread. The container will
 *                    perform the necessary error handling when
 *                    Servlet.service() exits.
 * ERROR            - Something went wrong.
 *
 *
 * The valid state transitions are:
 *
 *                  post()                                        dispatched()
 *    |-------»------------------»---------|    |-------«-----------------------«-----|
 *    |                                    |    |                                     |
 *    |                                    |    |        post()                       |
 *    |               post()              \|/  \|/       dispatched()                 |
 *    |           |-----»----------------»DISPATCHED«-------------«-------------|     |
 *    |           |                          | /|\ |                            |     |
 *    |           |              startAsync()|  |--|timeout()                   |     |
 *    ^           |                          |                                  |     |
 *    |           |        complete()        |                  dispatch()      ^     |
 *    |           |   |--«---------------«-- | ---«--MUST_ERROR--»-----|        |     |
 *    |           |   |                      |         /|\             |        |     |
 *    |           ^   |                      |          |              |        |     |
 *    |           |   |                      |    /-----|error()       |        |     |
 *    |           |   |                      |   /                     |        ^     |
 *    |           |  \|/  ST-complete()     \|/ /   ST-dispatch()     \|/       |     |
 *    |    MUST_COMPLETE«--------«--------STARTING--------»---------»MUST_DISPATCH    |
 *    |                                    / | \                                      |
 *    |                                   /  |  \                                     |
 *    |                    OT-complete() /   |   \    OT-dispatch()                   |
 *    |   COMPLETE_PENDING«------«------/    |    \-------»---------»DISPATCH_PENDING |
 *    |        |      /|\                    |                       /|\ |            |
 *    |        |       |                     |                        |  |post()      |
 *    |        |       |OT-complete()        |           OT-dispatch()|  |            |
 *    |        |       |---------«-------«---|---«--\                 |  |            |
 *    |        |                             |       \                |  |            |
 *    |        |         /-------«-------«-- | --«---READ_WRITE--»----|  |            |
 *    |        |        / ST-complete()      |        /  /|\  \          |            |
 *    |        |       /                     | post()/   /     \         |            |
 *    |        |      /                      |      /   /       \        |            |
 *    |        |     /                       |     /   /         \       |            |
 *    |        |    /                        |    /   /           \      |            |
 *    |        |   /                         |   |   /             \     |            |
 *    |        |  /                          |   |  /  ST-dispatch()\    |            |
 *    |        |  |                          |   | |                 \   |            |
 *    |  post()|  |  timeout()         post()|   | |asyncOperation()  \  |  timeout() |
 *    |        |  |  |--|                    |   | |                  |  |    |--|    |
 *    |       \|/\|/\|/ |     complete()    \|/ \|/|   dispatch()    \|/\|/  \|/ |    |
 *    |--«-----COMPLETING«--------«----------STARTED--------»---------»DISPATCHING----|
 *            /|\  /|\                       | /|\ |                       /|\ /|\
 *             |    |                        |  |--|                        |   |
 *             |    |               timeout()|  post()                      |   |
 *             |    |                        |                              |   |
 *             |    |       complete()      \|/         dispatch()          |   |
 *             |    |------------«-------TIMING_OUT--------»----------------|   |
 *             |                                                                |
 *             |            complete()                     dispatch()           |
 *             |---------------«-----------ERROR--------------»-----------------|
 *
 *
 * Notes: * For clarity, the transitions to ERROR which are valid from every state apart from
 *          STARTING are not shown.
 *        * All transitions may happen on either the Servlet.service() thread (ST) or on any
 *          other thread (OT) unless explicitly marked.
 * </pre>
 */
class AsyncStateMachine {

https://github.com/apache/tomcat/blob/10.1.36/java/org/apache/coyote/AsyncStateMachine.java#L30-L129

おわりに

Jakarta Servletの非同期凊理をWildFly 35.0.1.Final.、Apache Tomcat 10.1.36で詊しおみたした。

ほが觊れたこずがなかったのず、Jakarta Servletの仕様曞もいろいろず読むこずになったので勉匷になりたした。

ディスパッチたわりは今回は芋れなかったので、たたの機䌚に芋おみようかなず思いたす。