swagger-codegen テンプレートとロジックのカスタマイズ

swagger-codegenのテンプレとロジックのカスタマイズ

前回の記事から引き続き swagger-codgenをさわっていきます.

今回はswagger-codegen(以下codegen)のカスタマイズ方法を記載します. カスタマイズ方法については以下の記事を参考にしていますが, この記事に乗っていないことをなるべく記述していきたいと思います!

こんなに簡単! Swagger Codegenのカスタマイズ https://qiita.com/Quramy/items/c583f3213f0b77ff1bac

なお最終的なファイル構成をgithubに上げているのでそちらも併せてご確認ください.

既存のテンプレートで動かす

まずはカスタマイズする前にどのような出力が行われるかを確認してみます. 前回の記事にもjarを使用した実行方法を記載していますが,今回はgradleを使用した方法を紹介します.

gradleの準備

gradleは依存解決+ビルドを行ってくれる支援ツールです. jvm系言語のビルドによく使用されますがC++などのビルドにも対応しているらしいです. 公式ページに記載があるように,パッケージマネージャからinstallするのがおすすめです.

macの場合はbrewで,windowsの場合はchocolaty経由でインストールしましょう. (chocolatyはwindows向けのパッケージマネージャです.インストールもinstallにあるスクリプトをcmdに張り付けるだけなのでぜひ導入しましょう.)

 brew install gradle # mac
 choco install gradle # windows

設定ファイルの準備

codegenを実行するためのbuild.gradleを任意のディレクトリに用意します. また生成に使用するサンプルとしてここからサンプルのSwagger定義を取得してswagger.jsonとして保存します

build.gradleはgradle用の設定ファイルです.

plugins {
  id 'org.hidetake.swagger.generator' version '2.12.0'
}

repositories {
  jcenter()
}

dependencies {
  // Add dependency for Swagger Codegen CLI
  swaggerCodegen 'io.swagger:swagger-codegen-cli:2.3.1'
}

swaggerSources {
  example{
    inputFile = file('swagger.json') //swagger定義ファイル
    code {
      language = 'spring'  //生成先のフレームワーク/言語
    }
  }
}

この設定を用意することでgradle-swagger-generator-plugin を利用してcodegenを動作させられます. また同時にgroovyを使用できるようプラグインを設定しています.

実行

以下のコマンドでcodegenプラグインを実行できます.

gradle generateSwaggerCode

これでbuildディレクトリ以下にソースコードが生成されました. 生成対象はbuild.gradlelanguage = 'spring'と設定したため,springbootを使用したサーバサイドのコードが生成されました.

# tree build
build
└── swagger-code-example
    ├── README.md
    ├── pom.xml
    └── src
        └── main
            ├── java
            │   └── io
            │       └── swagger
            │           ├── RFC3339DateFormat.java
            │           ├── Swagger2SpringBoot.java
            │           ├── api
            │           │   ├── ApiException.java
            │           │   ├── ApiOriginFilter.java
            │           │   ├── ApiResponseMessage.java
            │           │   ├── NotFoundException.java
            │           │   ├── PetApi.java
            │           │   ├── PetApiController.java
            │           │   ├── StoreApi.java
            │           │   ├── StoreApiController.java
            │           │   ├── UserApi.java
            │           │   └── UserApiController.java
            │           ├── configuration
            │           │   ├── CustomInstantDeserializer.java
            │           │   ├── HomeController.java
            │           │   ├── JacksonConfiguration.java
            │           │   └── SwaggerDocumentationConfig.java
            │           └── model
            │               ├── Category.java
            │               ├── ModelApiResponse.java
            │               ├── Order.java
            │               ├── Pet.java
            │               ├── Tag.java
            │               └── User.java
            └── resources
                └── application.properties

language='go'と設定してやるとgolang向けのクライアントのコードを生成できます.

# tree build
build
└── swagger-code-example
    ├── README.md
    ├── api
    │   └── swagger.yaml
    ├── api_client.go
    ├── api_response.go
    ├── category.go
    ├── configuration.go
    ├── docs
    │   ├── Category.md
    │   ├── ModelApiResponse.md
    │   ├── Order.md
    │   ├── Pet.md
    │   ├── PetApi.md
    │   ├── StoreApi.md
    │   ├── Tag.md
    │   ├── User.md
    │   └── UserApi.md
    ├── git_push.sh
    ├── model_api_response.go
    ├── order.go
    ├── pet.go
    ├── pet_api.go
    ├── store_api.go
    ├── tag.go
    ├── user.go
    └── user_api.go

カスタマイズしたい対象を見つける

カスタマイズ対象のピックアップ

さて今回のテーマはカスタマイズですが,まずはカスタマイズしたい個所をピックアップします. codegenのテンプレートは様々な言語に対応していますが 中には手で修正しないとビルドに成功しないテンプレートもあります. 今回はcodegenの設定を以下のように設定してコードを生成します.

build.gradle

//抜粋
  example{
    inputFile = file('mytemplate/ninja.yaml') //swagger定義
    code {
      language = 'java' 
      configFile = file('config.json')
    }
  }

mytemplate/ninja.yaml

# 抜粋
definitions:
  Person:
    type: "object"
    required:
    - "name"
    properties:
      id:
        type: "integer"
        format: "int64"
      name:
        type: "string"
        example: "doggie"
      group:
        type: "string"
        description: "pet status in the store"
        enum:
        - "伊賀"
        - "甲賀"
        - "はぐれ"

すると,コンパイルした際に以下のようなエラーが出ます.

 警告: 識別子として'_'が使用されました
    _("伊賀"),

これはEnumクラスの定義として以下のようなコードが出力されているためです.

  /**
   * pet status in the store
   */
  @JsonAdapter(GroupEnum.Adapter.class)
  public enum GroupEnum {
    _("伊賀"),
    
    _("甲賀"),
    
    _("はぐれ");

    private String value;

すこし調べたところ,これはswagger定義にあるEnumを以下のようにEnumの定数名に変換しているためでした.

String var = value.replaceAll("\\W+", "_").toUpperCase();

当該コード

この処理で全角文字をすべて_に変換しているために起きている問題のようです. 今回はこの問題を回避するようなカスタマイズを行います. (ちなみにこの全角をEnumに設定するとうまくコード生成されないのがただのバグなのかSwaggerの定義にEnumに全角を設定してはいけないせいなのかは知りません!だれか詳しい人がいたら教えてください!!!><)

カスタマイズをはじめる

テンプレートのカスタマイズ

codegen参考記事にあるようにテンプレートとそのテンプレートに流し込むデータを作るロジック部に大別されます. イメージとしては以下のような感じです.

(swagger定義yaml)->ロジック部->(中間データ構造)->mustacheテンプレート

language毎のテンプレートファイルはcodegenのリポジトリのresouces以下で確認できます. language=javaの場合のテンプレートはこれです.

さて問題のコードがどのテンプレートファイルから生まれたのかをgrep等を使用して探してみると,どうやらmodelInnerEnum.mustacheが問題のコードに対応するようです.

テンプレートは一部のファイルだけのカスタマイズが可能なため,modelInnerEnum.mustachemytemplate/template/以下に保存し, build.gradleを以下のように設定します. こうすることでもともとのテンプレートをtemplateDir以下の同名ファイルでオーバーライドすることができます.

// 抜粋
  example{
    inputFile = file('mytemplate/ninja.yaml') //swagger定義
    code {
      language = 'java' 
      templateDir = file('mytemplate/template/')
    }
  }

Mustacheの文法に注意しながらテンプレートを適当に編集して保存します.

mytemplate/template/modelInnerEnum.mustache

   public enum {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} {
     {{#allowableValues}}
       {{#enumVars}}
+      // こんなこめんとをいれました
     {{{name}}}({{{value}}}){{^-last}},
     {{/-last}}{{#-last}};{{/-last}}
       {{/enumVars}}

この状態でgradle generateSwaggerCodeをしたら以下のように出力が変化しています.

  /**
   * pet status in the store
   */
  @JsonAdapter(GroupEnum.Adapter.class)
  public enum GroupEnum {
      // こんなこめんとをいれました
    _("伊賀"),
    
      // こんなこめんとをいれました
    _("甲賀"),
    
      // こんなこめんとをいれました
    _("はぐれ");

    private String value;

このようにテンプレートに追加した文字列がそのまま出力されるため, 簡単なカスタマイズであればこのテンプレートのカスタマイズだけで十分目的を果たせると思います.

ロジックのカスタマイズ

さて先ほど

(swagger定義yaml)->ロジック部->(中間データ構造)->mustacheテンプレート

と紹介しましたが,このようにテンプレートに埋め込む中間データ構造の値を変化させたい場合はロジックのカスタマイズが避けられません.

ロジック部のデータモデル

さてロジック部において各プロパティはCodegenPropertyクラスで扱われています.今回はこのへんenumVars.nameを別名で上書きすることにします.

Vender Extensions

ところで,Swagger定義では,Extensionsと呼ばれるx-から始まるカスタムプロパティを様々な項目に設定することができます. こちらの記事では,このextensions(Vender Extensions)を使用して中間データ構造に値を設定し,その値をテンプレートで参照することでカスタマイズを行っています. 今回はこのextension機能を利用してCodegenPropertyに値を渡し,ロジック内で特定のextensionがついていた場合はその値ででEnumの定数名を上書きします.

ロジックの継承とオーバライド

さきほどからlanguage='hoge'のように出力先の名前を指定していますが, ここの設定にはCodegenConfigインターフェースを継承したクラスのFQCN(パッケージ名とクラス名)を指定すればそのクラスを使用してくれます. したがって,カスタマイズしたいクラスを継承したクラスを自分で定義してそれをクラスパスに追加することで,任意の処理を既存の処理に挟み込むことができます.

というわけでlanguage='java'の際に使用されるJavaClientCodegenを継承したクラスをGroovyで定義してコンパイルしてクラスパスに追加して,そのクラス名をlanguageに指定して実行することでロジックのカスタマイズができました. (最終的ファイルをgithubに上げておいたので参考にしてください)

mytemplate/logic/MySwaggerLogic.groovy

import io.swagger.codegen.*;
import io.swagger.codegen.languages.*;

class MySwaggerLogic extends JavaClientCodegen {
    @Override
    public void updateCodegenPropertyEnum(CodegenProperty var) {
        super.updateCodegenPropertyEnum(var)
        if(var.vendorExtensions.containsKey("x-enum-names")){
            def alternames=var.vendorExtensions['x-enum-names']
            def enums=var.allowableValues["enumVars"]
            if(alternames.size()!=enums.size()){
                return
            }
            alternames.eachWithIndex{str,i->
                enums[i]["name"]=str
            }
        }
    }
}

build.gradle

plugins {
  id 'groovy'  // groovyでロジックを記述する際に使用
  id 'org.hidetake.swagger.generator' version '2.12.0'
}

repositories {
  jcenter()
}

dependencies {
  // Add dependency for Swagger Codegen CLI
  swaggerCodegen 'io.swagger:swagger-codegen-cli:2.3.1'
  // groovyのクラスファイルをクラスパスに追加
  swaggerCodegen files('build/classes/main/')
  swaggerCodegen localGroovy()
  // groovyコンパイル用の設定
  compile 'io.swagger:swagger-codegen-cli:2.3.1'
  compile localGroovy()
}

swaggerSources {
  example{
    inputFile = file('mytemplate/ninja.yaml') //swagger定義
    code {
      language = 'MySwaggerLogic' 
      configFile = file('config.json')
      templateDir = file('mytemplate/template/')
    }
  }
}
// swaggerSourceの前にgroovyをコンパイル
swaggerSources.example.code.dependsOn compileGroovy 
//ロジック記述ファイルの場所
sourceSets.main.groovy.srcDirs=['mytemplate/logic']

結果

  /**
   * pet status in the store
   */
  @JsonAdapter(GroupEnum.Adapter.class)
  public enum GroupEnum {
      // こんなこめんとをいれました
    Iga("伊賀"),
    
      // こんなこめんとをいれました
    Kouga("甲賀"),
    
      // こんなこめんとをいれました
    Alone("はぐれ");

    private String value;

想定したとおりにカスタマイズすることができました.

まとめと補足

本記事ではswagger-codegenを使用してコードを生成する方法,そしてテンプレートとロジックのカスタマイズ方法を解説しました.

個人的には前回の記事 にscaffoldingにcodegenを使ってみてはと書いた手前あれですが,用意されているテンプレートによってはいろいろなカスタマイズが必要になるのでなかなか良しあしがあるように感じます.本当に最初だけにとどめてそのあとは直接生成したコードを編集するのが楽な気がします...

補足

  • codegenのテンプレートはバージョンが変わるとところどころ変わっていることがあるので,github等でテンプレートを見る際は手元のcodegenのバージョンに注意してください
  • swagger仕様には2.0,3.0,3.1?の主なバージョンがありますが,codegenの3.0対応はあまり進んでいないらしい上にそれが原因でフォークプロジェクトができているようです

速習プリパラ1クール目

概要

今からプリパラを見直す際に(個人的に思う)見るべき話数を上げていきます. A プリパラシリーズ上必要 B そのクール上は見たほうがいいかも C 正直ちゃんとみてない

1クール目 ソラミスマイル編

1 アイドル始めちゃいました

A 記念すべき一話. みれぃの「プリパラは好きプリ?」は後々多用されるセリフであるが. この時点ではわりとさっらとやりとりされているね

2 約束やぶっちゃダメぷりっ

A らぁらとみれぃのコンビ結成回. みれぃとらぁらの関係性の基礎となるような話なので必須

3 チーム解散?困るクマ~!

C 練習回でもあり,そふぃ初登場?回でもあり, 校長の友達嫌いがわかる会でもある. 急ぎ目であればいらないかな?

4 かしこま!元気 For You

C テニス組,校長,のんが活躍する話 友達との約束うんぬん~みたいな友情な話, 女児アニメにはこういう話が大事ね

5 あたし、そふぃさんと歌いたいワニ!

B そふぃ回. そふぃ,ちゃんこ,こすもが登場する会. そふぃの人格説明回なのでとばすとわかりづらくなるかも. そふぃ勧誘をみれぃが認めた回でもある. ここからしばらくそふぃを追いかける話なので大事かもね

6 異議あり?らぁらがウチにやってきたっぷり!

C みれいの家訪問回. 正直両親はどうでもいいが, 2人が想像するアイドル検事みれい像の画像がシリーズでちょくちょく出てきた気がする. 新曲お披露目会かつMD作成会でもある. みれぃの昔の写真がMDの発想素だったのね(見直して知った)

7 レッドフラッシュを探して・・・

B そふぃ,うさぎ,校長回? 校長が次クールに向けてめっちゃスタンバってますね そふぃとらぁらのからみ2 そふぃとトモチケを交換するので期待させてからの

8 ドキドキ!夏だ!水着だ!プールでかしこまっ♪

C 幼馴染のなお回 シリーズを通してちょくちょくある 仲たがい...いいね 女児アニメ回 あの演説からこの曲であるみたいな気持ちにならないか? みれぃのアイドルは友達も笑顔に~のセリフは12話に使われる

9 ときめきアイドル大集合!

A そふぃとらぁらのからみ3 キュピコン初登場?

そふぃの出自の話なのでみておいたほうがよいかも トモチケ交換で期待させておいてからの

10 秋色ラブリーライブ

C 新しいコーデを考えながららぁらがぼけ倒す会 裏でそふぃの挫折が描かれていて1クール目の終わりまでの土台となるが見なくてもわかるか チーム結成もそふぃに断られるはなし

11 どうする?どうなる!?3人目!!!

B 1クール目終わりに向けて大事 メイキングドラマの振りが次の会で回収される

12 はばたけ、そふぃ!

A ソラミスマイル結成回 みれぃはそふぃのこと嫌いだな? ドロシー&レオナ登場回でもあるし2クール目に必要な話 初3人ライブ

13 空見て笑って・チーム名発表!

A チーム名決定話 ぷりぷりかしこま そふぃとみれぃの話でもある ファルルが出てくる

14 ライバル登場!イゴ、よろしく!!

A シオンが参戦! れっついごー好き ドレッシングパフェ結成回&初ライブ 初見はかなりインパクトあった

15 一触即発?シオンVSみれぃぷりっ!

C シオン回 初ソラミVSドレシではあるが 話は大きく進まない

続く!?

swagger-codegenですわっとscaffoldingせん

こんにちは. 今回は(swagger-codegen)https://github.com/swagger-api/swagger-codegenを使ったりカスタマイズしてみようの会です.

swagger-codegenとは?

リポジトリのへのリンク github - swagger-codegen

Overview This is the swagger codegen project, which allows generation of API client libraries (SDK generation), server stubs and documentation automatically given an OpenAPI Spec. Currently, the following languages/frameworks are supported:

swagger-codegenは,swaggerエコシステムのツールの一つであり, OpenAPI3.0仕様(もしくはSwagger2.0仕様)から, クライアントライブラリやサーバのスタブ, ドキュメントを生成するツールです.

swagger自体とその周辺ツールについては, おおむね知っている前提で話を進めていきます.

swagger-codegenを使う動機

今回はサーバ側コードのscaffoldingを行うことを目的にswagger-codgenを使用します.

リポジトリのreadmeにあるように,基本的にはswagger-codegenで生成したものは プロダクション環境ではドキュメントとクライアントライブラリのみを使用することを想定されています(サーバはスタブのみ)

しかしながら,スタブ生成の出力先言語/フレームワーク一覧を見てみると, 以下のようになっています.

Server stubs: Ada, C# (ASP.NET Core, NancyFx), C++ (Pistache, Restbed), Erlang, Go, Haskell (Servant), Java (MSF4J, Spring, Undertow, JAX-RS: CDI, CXF, Inflector, RestEasy, Play Framework, PKMST), Kotlin, PHP (Lumen, Slim, Silex, Symfony, Zend Expressive), Python (Flask), NodeJS, Ruby (Sinatra, Rails5), Rust (rust-server), Scala (Finch, Lagom, Scalatra)

注目したのは

  • MSF4J
  • JAX-RS: CDI, CXF, Inflector, RestEasy

のように,scafoldingに対応していないフレームワークのコード生成も行ってくれることです. そこで,今回はswagger-codegenでサーバスタブを生成し, そのコードを足場(scafold!)として開発を行っていきます.

やってみよう!

コード生成自体は,Open API仕様のjsonyamlを用意すれば簡単に行えます. 今回はswaggerプロジェクト公式で配布しているサンプルを使用します.

1. OpenAPI仕様のファイルの準備

サンプルへのリンク このリンクから今回使用するサンプルファイルを取得します. 自分で用意する場合は,swagger-editor等のツールを使用して用意するのがいいと思います. ~(ただ正直swagger-editorを使ってもかなり定義する部分があるので結構大変です)~

2. コード生成

コード生成はおおむね2つの方法があります.

  1. swagger-editorで生成
  2. ローカルでビルド or jarを取得して生成

swagger-editorで生成

先ほど紹介したswagger-editorにコード生成機能が実装されています.editorにjson or yamlをはりつけて上部リンクの Generate ServerGenerate Clientから生成対象の言語/フレームワークを選択すれば,生成されたコードがzip圧縮したものが入手できます.

ローカルで生成

swagger-codegenのreadme内getting Startedにあるように,ソースコードを取得してからビルドすることで,実行可能jarを入手できます.ただ,ビルド済みのjarがmavenリポジトリ 上に配置されているため,ここからjarを取得する方が早いです.

コード生成コマンド

jarを取得した後は,以下のコマンドでコード生成ができます.

java -jar swagger-codegen-cli.jar generate -i [swagger.yaml or json]\
   -l [生成対象の言語/フレームワーク] \
   (-o [生成先フォルダ]\)
   (-c [config.jsonへのパス]\)
生成可能言語/フレームワーク一覧表示

生成可能な対象は以下のコマンドでリストを見ることができます

java -jar swagger-codegen-cli.jar langs 
生成対象言語/フレームワーク毎の設定項目表示

生成対象によってはconfig.jsonでオプションを設定できます. 例えばjava-play-framework向けの設定項目を調べるには以下のコマンドを使います.

 java -jar .\hoge.jar  config-help -l java-play-framework
生成結果

実際にコードを生成すると,フレームワークに応じたファイルが出力されます. これら生成されるコードを利用することでmodelやapiのための記述量を減らすことができます.

 java -jar .\hoge.jar  generate  -l java-play-framework -i http://petstore.swagger.io/v2/swagger
.json -o out
[main] INFO io.swagger.parser.Swagger20Parser - reading from http://petstore.swagger.io/v2/swagger.json
...

└─out
    ├─.swagger-codegen
    ├─app
    │  ├─apimodels
    │  ├─controllers
    │  └─swagger
    ├─conf
    ├─project
    └─public

生成されるコードについては生成対象ごとのテンプレート次第です. なので気に食わなかった場合はテンプレートのカスタマイズ,ロジックのカスタマイズを行っていきましょう.

後半(カスタマイズ編)に続く(予定)

参考リンク

nginxのソースコードを読む1

今回はnginxのメインループをたどっていきます。

ソースコードの入手

ソースコードは(ここ)https://nginx.org/en/download.htmlからダウンロードできます。 今回は1.11.2を利用しています。

ソースのディレクトリ構造は以下の通りです。

/
├── core(エントリポイントや内部で汎用的に使用するデータ構造等)
├── event(イベント処理の仕組み)
│   └── modules(イベントを通知する際に選べるシステムコール毎のモジュール実装)
├── http(httpを処理する部分?)
│   ├── modules(ssl, auth, アクセス制限等configで設定するような細かな項目のモジュール)
│   │   └── perl(perlモジュール?)
│   └── v2(不明)
├── mail(メール関係 stmp, imap等)
├── misc(google_perftoolとそのためのc++用の何かがある)
├── os
│   └── unix(ソケットやforkの処理等システムコールを用いる処理)
└── stream(不明)

ここからざっくばらんにnginxのソースコードを読んでいきます。

エントリポイントからメインループまで

nginxはmaster-worker構成のためメインループが複数存在することになりますが、 まずはそれぞれのメインループに入るまでと入ってからの処理を大まかに観ていきたいと思います。

まずnginx起動時、/core/nginx.cのmain関数から実行されます。

//core/nginx.c
main(int argc, char *const *argv){
/* ポインタ作成 */
/* debugとerrorの初期化 */
/* オプションのパース */
/* 各種初期化及びシグナル送信等*/
 if (ngx_process == NGX_PROCESS_SINGLE) {
    ¦   ngx_single_process_cycle(cycle);

    } else {
    ¦   ngx_master_process_cycle(cycle);
    }

    return 0;
}

いろいろ省略していますが、 -vや-tや-s等のすぐに処理が終わる場合でなければ、 上記のngx_single_process_cycleかngx_master_process_cycleが呼ばれます。 single_processは詳細は知りませんがnginxをシングルプロセスで動かすオプションを つけた場合に実行されると思われます。 そこで今回はmaster_processの方に注目して追っていきます。

ngx_master_process_cycleは/os/unix/ngx_process_cycle.cに定義されています。

/os/unix/ngx_process_cycle.c

void
ngx_master_process_cycle(ngx_cycle_t *cycle){
/* 変数の初期化 */
/* シグナル受信時の処理を設定 */
/* プロセスのタイトル等設定 */
 ngx_setproctitle(title);

/* コンフィグの読み取り */
    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
/* ワーカプロセスの起動とキャッシュマネージャの起動予約 */
    ngx_start_worker_processes(cycle, ccf->worker_processes,NGX_PROCESS_RESPAWN);
    ngx_start_cache_manager_processes(cycle, 0);

/* masterのメインループ */
 for ( ;; ) {
/* 何か */
sigsuspend(&set);
/* 何か */
if (ngx_reap) {
/* 子プロセスの削減? */
 live = ngx_reap_children(cycle);
}
if (!live && (ngx_terminate || ngx_quit)) {
/* マスタの終了 */
ngx_master_process_exit(cycle);
}

if (ngx_terminate) {
/* 子を終了させる */
/* 省略 */
 if (delay > 1000) {
 ngx_signal_worker_processes(cycle, SIGKILL);
} else {
 ngx_signal_worker_processes(cycle,
 ngx_signal_value(NGX_TERMINATE_SIGNAL));
 }
continue;
}

if (ngx_quit) {
/* 子プロセスへシャットダウンシグナル */
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_SHUTDOWN_SIGNAL));

ls = cycle->listening.elts;
  for (n = 0; n < cycle->listening.nelts; n++) {
     if (ngx_close_socket(ls[n].fd) == -1) {
     ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno,
    ngx_close_socket_n " %V failed",
   &ls[n].addr_text);
    }
     }
     cycle->listening.nelts = 0;

    continue;
    }

    if (ngx_reconfigure) {
/* 設定ファイルのリロード */
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,
            ngx_core_module);
     ngx_start_worker_processes(cycle, ccf->worker_processes,
    NGX_PROCESS_JUST_RESPAWN);
     ngx_start_cache_manager_processes(cycle, 1);

    /* allow new processes to start */
     ngx_msleep(100);

    live = 1;
     ngx_signal_worker_processes(cycle,
      ngx_signal_value(NGX_SHUTDOWN_SIGNAL));

    if (ngx_restart) {
   ngx_restart = 0;
    ngx_start_worker_processes(cycle, ccf->worker_processes,
     NGX_PROCESS_RESPAWN);
      ngx_start_cache_manager_processes(cycle, 0);
   live = 1;
     if (ngx_reopen) {
/* ログファイルのリオープン */

    if (ngx_change_binary) {
/* バイナリを起動しながら交換 gracefull*/
    if (ngx_noaccept) {
/* 何か */

}
}

これがマスタプロセスの起動時の動きとメインループ内の処理です。 基本的には子プロセスとキャッシュマネージャの起動予約※をした後、 シグナルを受け付けるだけになります。 マスタプロセスが子を制御しかしていないことがよくわかりますね。 ※キャッシュマネージャは常に存在しているわけではないので定期的に起動しているはず

子プロセスの起動

次にngx_start_worker_processesで何しているかを観ます。 ほげ この関数は先ほどと同じ/os/unix/ngx_process_cycle.cに記述されています。

/os/unix/ngx_process_cycle.c

static void
ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)
{
/* ログ記入、変数初期化 */
 ngx_channel_t    ch;

 for (i = 0; i < n; i++) {

    ¦   ngx_spawn_process(cycle, ngx_worker_process_cycle,
    ¦   ¦   ¦   ¦   ¦   ¦ (void *) (intptr_t) i, "worker process", type);

    ¦   ch.pid = ngx_processes[ngx_process_slot].pid;
    ¦   ch.slot = ngx_process_slot;
    ¦   ch.fd = ngx_processes[ngx_process_slot].channel[0];

    ¦   ngx_pass_open_channel(cycle, &ch);
   }
}

観ての通りngx_spawn_processを起動したい数だけ実行したのちにチャンネルの設定を行っています。ngx_spawn_processはnginx内部でプロセス生成する際に用いる汎用的な関数です。第二引数に生成したプロセスで実行したい関数へのポインタを指定します。このプロセス生成部については後程取り上げます。 ngx_process_slotは生成したプロセスの通し番号のため生成したばかりのプロセスの情報をchに格納していることになります。(たぶん生成したプロセスの遠し番号だと思う)

ここの子プロセス生成部で着目すべきはとngx_pass_open_channelです。少し追ってみましょう。(※本当か?)

ngx_pass_open_channelは同じ/os/unix/ngx_process_cycle.cにありまぁす

/os/unix/ngx_process_cycle.c

static void
ngx_pass_open_channel(ngx_cycle_t *cycle, ngx_channel_t *ch)
{
    ngx_int_t  i;

    for (i = 0; i < ngx_last_process; i++) {

    ¦   if (i == ngx_process_slot
    ¦   ¦   || ngx_processes[i].pid == -1
    ¦   ¦   || ngx_processes[i].channel[0] == -1)
    ¦   {
    ¦   ¦   continue;
    ¦   }

    ¦   ngx_log_debug6(NGX_LOG_DEBUG_CORE, cycle->log, 0,
    ¦   ¦   ¦   ¦   ¦ "pass channel s:%i pid:%P fd:%d to s:%i pid:%P fd:%d",
    ¦   ¦   ¦   ¦   ¦ ch->slot, ch->pid, ch->fd,
    ¦   ¦   ¦   ¦   ¦ i, ngx_processes[i].pid,
    ¦   ¦   ¦   ¦   ¦ ngx_processes[i].channel[0]);

    ¦   /* TODO: NGX_AGAIN */

    ¦   ngx_write_channel(ngx_processes[i].channel[0],
    ¦   ¦   ¦   ¦   ¦   ¦ ch, sizeof(ngx_channel_t), cycle->log);
    }
}

ここの処理は子プロセスを生成するごとに行われます。 ①既存のプロセスをひとつ取り出す ①-a 自分自身か無効なプロセスな場合continue ①-b 取り出したプロセスと生成したプロセスを引数にngx_write_channel

個々から見たいもの
ngx_write_channel
ngx_spawn_process(ngx_process_slot)
ngx_channel_t

つづく

キャッシュについてまとめ

キャッシュについてまとめ

基本

リクエストが増えるとIO待ちが増えて処理時間の低下を引き起こす 多くのWebサービスでは少数のファイルにリクエストが集中する この参照頻度の偏りを利用する - 頻度の多いファイル⇒高速なデバイス - その他⇒ふつうのデバイ

用語 - キャッシュサーバ - オリジンサーバ

キャッシュサーバとしての設定と クライアントにキャッシュさせる場合の設定がある。 (ヘッダフィールドの指定等)

基本的にキャッシュとオリジナルコンテンツと同じサーバにある場合その有効性は限定的。 (コンテンツがすべてメモリのページキャッシュに乗っている場合キャッシュでなくてもメモリから高速に応答できるため)

キャッシュに関するディレクティブ

ngx_http_proxy_moduleに実装されている (proxyと書いてあるがnginxのみの静的ファイル配信の際にもこれを用いる)

proxy_cache_path 保存先パス keys_zone=キーゾーン名:サイズ

キャッシュの保存先とキーゾーンの設定

proxy_cache キーゾーン名|off

キャッシュに利用するキーゾーンを指定する

proxy_cache_key キャッシュキー文字列;

キャッシュのキーに使用する文字列を指定する

キャッシュの基本

proxy_cache_pathによってキャッシュファイルの保存先を指定する。 キーゾーン名やその容量などのパラメタを指定する。 どのキーゾーンに保存するかはproxy_cacheディレクティブを使用する。 proxy_cache_keyでキャッシュのキーとして使用する値を使用する。 子の値をキーとしてキャッシュの同一性を確認する。 (デフォルトはスキーム、プロキシ先のホスト名、リクエストされたURIをキーに使用する)

補足

  • プロセス間でキャッシュを共有するためキャッシュのメタデータを共有メモリに保存する。
  • ゾーンの必要容量=キャッシュの総容量÷1ファイルの平均サイズ*128B
  • キャッシュファイルの保存ディレクトリの内部階層の指定をできる

キャッシュマネージャの制御

proxy_cache_pathディレクティブのloader_files, loader_sleep, loader_threasholdでキャッシュマネージャを制御できる

有効期限の指定

  1. キーゾーンごとのキャッシュ有効期限を設定する
  2. オリジンサーバのレスポンスヘッダに有効期限を指定する
  3. レスポンスヘッダで指定されなかった場合のhttpステータスコードごとの有効期限を指定する

  4. キーゾーン
    proxy_cache_pathにinactiveパラメータを指定する
    指定した有効期限より長くリクエストされないとキャッシュファイルが削除される。
    デフォルト値は10分間

2.レスポンスヘッダ - X-Accel-Expires - Cache-Control - Expires これらのヘッダフィールドをオリジンサーバからのレスポンスに付加することでキャッシュの有効期限を指定できる 3.ステータスコード毎に有効期限を設定 proxy_cache_valid [HTTPステータスコード] 有効期限; これを設定することでステータスコードごとに有効期限を指定できる

キャッシュ条件の指定

nginxはデフォルトでGET, HEADメソッドのレスポンスをキャッシュする。 (上記の設定をした場合の話) - GET,HEAD以外もキャッシュしたい場合 proxy_cache_methodsディレクティブを使う - URLでキャッシュするか決めたい場合locationディレクティブを使う - それ以外の時proxy_cache_bypassとproxy_no_cacheを使う proxy_cache_bypass 1 のときレスポンスをキャッシュから取得しない proxy_no_cache 1 の時 レスポンスをキャッシュに保存しない (変数を使って1 0 を切り替える)

一時ファイルの保存先の指定

nginxはキャッシュするレスポンスを一旦一時ファイルに保存し、その後指定したフォルダに移動する そのため、一時保存先と保存先が違うデバイスだとファイルをコピーする必要がありIOが発生する この一時ファイルの保存先はデフォルトはコンパイル時に指定 明示的に指定する場合proxy_temp_pathを使用する proxy_temp_path 一時ファイルの保存先パス

キャッシュ更新負荷の削減

キャッシュを利用した場合リクエスト数を減らすことができる しかし失効した瞬間にリクエストがバーストする。 キャッシュ失効は大きなネットワーク負荷やディスクIOの原因になりうる。

  • proxy_cache_revalidate on|off; キャッシュ更新の際にオリジンサーバに if-Modifed-Since, If-None-Matchヘッダフィールドで送って 更新されていなかった場合304が帰ってくるので、帯域負荷を抑えられる。

  • proxy_cache_lock on|off もう一つの方法としてキャッシュロックがある。 これを有効にすると同じURIに対してのアクセスを一つ目以外ブロックする。 これによりアップストリーム鯖への問い合わせ回数を制限できる このブロックのタイムアウトはproxy_cache_lock_timeoutで指定できて、デフォルトは5秒になっている。

    クライアントにキャッシュさせる場合の設定


クライアントにレスポンスをキャッシュさせるための設定はヘッダフィールドに設定を付加して行う。

  • add_header ヘッダフィールド名 値 [always] alwaysを付加すると404,500と行ったエラーの場合にもヘッダフィールドを付加できる。 (ヘッダの追加のみ行うためアップストリームで設定しているフィールドをaddしてしまうと重複して出力される) (重複されている場合の挙動は定義されていないため不具合の原因になる可能性がある)

  • 関係するヘッダフィールド

    • X-Accel-Expires 秒数 キャッシュサーバのnginxがコンテンツキャッシュの有効期限として解釈する (一般のブラウザでは解釈されない!!!)
    • Cache-Control
    • Expires HTTp/1.1ではこの二つのヘッダでキャッシュの制御を行う
  • Expiresヘッダの制御

    expires [modified] 有効期限;
    expires epoch|max|off;
    

    HHTP/1.1に準拠するヘッダはexpiresディレクティブで行う

    expires 10d; #10日間キャッシュを行う。
    

    デフォルトではリクエストされた時点の時刻を起点に計算される modifiedをつけるとファイルの最終更新時刻からの有効期限になる。

    expires modified 5d; #更新日時から5日までキャッシュされる
    

    epochを指定した場合Expiresヘッダフィールドの値が1970/00:00:00になる(最古の値) maxを指定した場合もっとも未来の値になる。 常に無効にしたい場合epoch、常に有効にしたい場合maxに指定する 通常はexpiresでExpriresを設定するだけで期限を設定できるが、他の細かい制御をするには Cache-Controlヘッダが必要なため場合によってはadd_headerでCache-Controlを指定する必要がある。

  • Cache-Controlヘッダ

    • max-age=秒数
    • no-cache #キャッシュしない
    • max-age=315360000 #未来の最大値(expiresのmaxと同じ)
    • private #キャッシュの共有を禁止(ブラウザレベルでしかキャッシュしないようにする)

    条件付きリクエスト


ブラウザ、キャッシュサーバのキャッシュ以外にトラフィックを最小化する手法として条件付きリクエストが存在する。 キャッシュの期限が切れた際にキャッシュが有効か確認をし、無効であれば新しいレスポンスを受け取る方法。

条件付きリクエストには以下のヘッダが付与されている - If-Modified-Since: "レスポンスのLast-Modifiedヘッダ" - If-None-Match: "レスポンスのETagヘッダフィールド"

サーバはこれらの値を確認し送信するファイルの内容が前回のキャッシュと変更されていないと判断できれば、 304(Not Modified)を送信しファイル送信を省略できる。 もし変更されていれば通常通りファイルの内容を返す。

一般的なブラウザは条件付きリクエストに対応しており、nginxでもproxy?cache_revalidateを使用すれば、 条件付きリクエストによるキャッシュの有効性確認ができる。

Last-Modified

リソースの最終更新日時を指定する nginxが静的リソースを配信する場合は自動的に付与される。 Webアプリケーションの出力などには自動的には付与されない。

ETag

リソースを一意に識別できるユニークな文字列を指定する nginxではetagディレクティブで制御できる。

etag on|off; #(デフォルトon)

nginxではファイルの更新日時とファイルサイズから自動的に計算される。 基本的にはEtagを増やした状態で構わない

秘伝のタレの解説

 location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
     try_files $uri @app;
     expires max;
     add_header Pragma public;
     add_header Cache-Control "public, must-revalidate, proxy-revalidate";
     etag off;
 }

画像ファイルのとき パスに存在すればそれを配信 クライアント側で永遠にキャッシュする privateの逆 privateの逆 必ずサーバに再認証を行う キャッシュサーバに対して再認証を要求 etagをつけない(←!?

etagがなくても条件付きリクエストをやるのかもしれない

nginxをよむ

ソースコードリーディングが何ぼのもんじゃい

core/nginx.c

  if (ngx_process == NGX_PROCESS_SINGLE) {
        ngx_single_process_cycle(cycle);

    } else {
        ngx_master_process_cycle(cycle);
    }

os/ngx_process_cycle.c

 ngx_start_worker_processes(cycle, ccf->worker_processes,
                               NGX_PROCESS_RESPAWN);
    ngx_start_cache_manager_processes(cycle, 0);

//後ろの方にはシグナルとかrestartの処理が書いてあった

static void
ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)
{
 ngx_spawn_process(cycle,ngx_worker_process_cycle,(void *) (intptr_t) i, "worker process", type);

}

os/unix/ngx_process.c

 ngx_pid_t
  ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
      char *name, ngx_int_t respawn){

//省略

    pid = fork();

    switch (pid) {

    case -1:
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "fork() failed while spawning \"%s\"", name);
        ngx_close_channel(ngx_processes[s].channel, cycle->log);
        return NGX_INVALID_PID;

    case 0:
        ngx_pid = ngx_getpid();
        proc(cycle, data);
        break;

    default:
        break;
    }

//省略

}

procが関数として呼び出されている
procの型はngx_spawn_proc_pt(なんだこりゃ

os/unix/ngx_process.h

typedef void (*ngx_spawn_proc_pt) (ngx_cycle_t *cycle, void *data);

ngx_spawn_proc_ptは上記二つの引数をとる関数の型

呼び出し元を見るとos/unix/ngx_process_cycle.cのngx_worker_process_cycle

os/unix/ngx_process_cycle.c


static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
//~
 ngx_process_events_and_timers(cycle);
//~
}
./event/ngx_event.c:194

void
ngx_process_events_and_timers(ngx_cycle_t *cycle){
//~
  (void) ngx_process_events(cycle, timer, flags);
//~
}
./event/ngx_event.h:408

#define ngx_process_events   ngx_event_actions.process_events

わった

検索結果

[root@hote src]# grep -rne ngx_event_actions ./
./event/modules/ngx_win32_select_module.c:106:    ngx_event_actions = ngx_select_module_ctx.actions;
./event/modules/ngx_devpoll_module.c:186:    ngx_event_actions = ngx_devpoll_module_ctx.actions;
./event/modules/ngx_epoll_module.c:368:    ngx_event_actions = ngx_epoll_module_ctx.actions;
./event/modules/ngx_eventport_module.c:279:    ngx_event_actions = ngx_eventport_module_ctx.actions;
./event/modules/ngx_kqueue_module.c:224:    ngx_event_actions = ngx_kqueue_module_ctx.actions;
./event/modules/ngx_poll_module.c:96:    ngx_event_actions = ngx_poll_module_ctx.actions;
./event/modules/ngx_select_module.c:105:    ngx_event_actions = ngx_select_module_ctx.actions;
./event/ngx_event.c:44:ngx_event_actions_t   ngx_event_actions;

環境と方式によって違うメソッドを実行している

/event/modules/ngx_eventport_module.c:279

static ngx_int_t
ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer)
{
//~
ngx_event_actions = ngx_epoll_module_ctx.actions;
//~
}


/event/ngx_event.c

static char *
ngx_event_core_init_conf(ngx_cycle_t *cycle, void *conf)
{
    ngx_event_conf_t  *ecf = conf;

#if (NGX_HAVE_EPOLL) && !(NGX_TEST_BUILD_EPOLL)
    int                  fd;
#endif
    ngx_int_t            i;
    ngx_module_t        *module;
    ngx_event_module_t  *event_module;

    module = NULL;

#if (NGX_HAVE_EPOLL) && !(NGX_TEST_BUILD_EPOLL)

    fd = epoll_create(100);

    if (fd != -1) {
        (void) close(fd);
        module = &ngx_epoll_module;

    } else if (ngx_errno != NGX_ENOSYS) {
        module = &ngx_epoll_module;
    }
}
|||<
切り替えをしているのはこの部分
(パラメータで切り替えるのになぜマクロ?)
(気力が失せた)


module_epoll
>||
ngx_event_module_t  ngx_epoll_module_ctx = {
    &epoll_name,
    ngx_epoll_create_conf,               /* create configuration */
    ngx_epoll_init_conf,                 /* init configuration */

    {
        ngx_epoll_add_event,             /* add an event */
        ngx_epoll_del_event,             /* delete an event */
        ngx_epoll_add_event,             /* enable an event */
        ngx_epoll_del_event,             /* disable an event */
        ngx_epoll_add_connection,        /* add an connection */
        ngx_epoll_del_connection,        /* delete an connection */
#if (NGX_HAVE_EVENTFD)
        ngx_epoll_notify,                /* trigger a notify */
#else
        NULL,                            /* trigger a notify */
#endif
        ngx_epoll_process_events,        /* process the events */
        ngx_epoll_init,                  /* init the events */
        ngx_epoll_done,                  /* done the events */
    }
};



このうちのactionsに対応する部分を実行している

events.h

typedef struct {
    ngx_str_t              *name;

    void                 *(*create_conf)(ngx_cycle_t *cycle);
    char                 *(*init_conf)(ngx_cycle_t *cycle, void *conf);

    ngx_event_actions_t     actions;
} ngx_event_module_t;

actionsに相当するのは関数のリストであるぞ

このリストの中でメインループなのはprocess_events
なかでepoll掘って完了したイベントごとに何かしている感じ