Skip to content

スケジュール・コンビネータ

スケジュールは、状態を持つ可能性のある効果的なイベントの再発スケジュールを定義し、さまざまな方法で構成します。コンビネータを使用すると、スケジュールを組み合わせて別のスケジュールを得ることができます。

さまざまなスケジュールの機能を示すために、次のヘルパーを使用します。

import { Effect, Schedule, TestClock, Fiber, TestContext } from "effect"
let start = 0
let i = 0
export const log = <A, Out>(
action: Effect.Effect<A>,
schedule: Schedule.Schedule<Out, void>
) => {
Effect.gen(function* () {
const fiber: Fiber.RuntimeFiber<[Out, number]> = yield* Effect.gen(
function* () {
yield* action
const now = yield* TestClock.currentTimeMillis
console.log(
i === 0
? `delay: ${now - start}`
: i === 10
? "..."
: `#${i} delay: ${now - start}`
)
i++
start = now
}
).pipe(
Effect.repeat(schedule.pipe(Schedule.intersect(Schedule.recurs(10)))),
Effect.fork
)
yield* TestClock.adjust(Infinity)
yield* Fiber.join(fiber)
}).pipe(Effect.provide(TestContext.TestContext), Effect.runPromise)
}
declare const log: <A, Out>(
action: Effect.Effect<A>,
schedule: Schedule.Schedule<Out, void>
) => void;

実装を表示するにはクリックしてください
// @include: Delay

log ヘルパーは、各実行の間の時間の遅延を記録します。このヘルパーを使用して、さまざまな組み込みスケジュールの動作を示します。

構成

スケジュールは、主に次の方法で構成されます:

  • Union. これにより、2 つのスケジュールの間隔の和集合が実行されます。
  • Intersection. これにより、2 つのスケジュールの間隔の共通部分が実行されます。
  • Sequencing. これにより、一方のスケジュールの間隔が他方に連結されます。

Union

2 つのスケジュールを和集合によって結合し、どちらかのスケジュールが再発を希望する限り、再発を続け、再発間の遅延の最小値を使用します。

Delay.ts
// @include: Delay
// @filename: index.ts
// ---cut---
import { Schedule, Effect } from "effect";
import { log } from "./Delay";
const schedule = Schedule.union(
Schedule.exponential("100 millis"),
Schedule.spaced("1 second")
);
const action = Effect.void;
log(action, schedule);
/*
Output:
delay: 0
#1 delay: 100 < exponential
#2 delay: 200
#3 delay: 400
#4 delay: 800
#5 delay: 1000 < spaced
#6 delay: 1000
#7 delay: 1000
#8 delay: 1000
#9 delay: 1000
...
*/

この組み合わせスケジュールを Effect.repeat で使用すると、2 つのスケジュール間の最小遅延に基づいて効果が繰り返し実行されることが観察されます。この場合、遅延は指数関数的スケジュール(遅延が増加する)の間と間隔のスケジュール(一定の遅延)の間で交互に変わります。

Intersection

2 つのスケジュールを共通部分によって結合し、両方のスケジュールが再発を希望する場合にのみ再発し、再発間の遅延の最大値を使用します。

Delay.ts
// @include: Delay
// @filename: index.ts
// ---cut---
import { Schedule, Effect } from "effect";
import { log } from "./Delay";
const schedule = Schedule.intersect(
Schedule.exponential("10 millis"),
Schedule.recurs(5)
);
const action = Effect.void;
log(action, schedule);
/*
Output:
delay: 0
#1 delay: 10 < exponential
#2 delay: 20
#3 delay: 40
#4 delay: 80
#5 delay: 160
(end) < recurs
*/

この組み合わせスケジュールを Effect.repeat で使用すると、効果が両方のスケジュールがそれを再発希望する場合にのみ繰り返されることが観察されます。再発間の遅延は、両方のスケジュール間の最大遅延によって決まります。この場合、遅延は指数関数的スケジュールの進行に従って増加し、再帰的スケジュールによって指定された最大回数の再発が達成されるまで続きます。

Sequencing

2 つのスケジュールを順次結合し、最初のポリシーが終了するまで従い、その後に 2 番目のポリシーに従います。

Delay.ts
// @include: Delay
// @filename: index.ts
// ---cut---
import { Schedule, Effect } from "effect";
import { log } from "./Delay";
const schedule = Schedule.andThen(
Schedule.recurs(5),
Schedule.spaced("1 second")
);
const action = Effect.void;
log(action, schedule);
/*
Output:
delay: 0
#1 delay: 0 < recurs
#2 delay: 0
#3 delay: 0
#4 delay: 0
#5 delay: 0
#6 delay: 1000 < spaced
#7 delay: 1000
#8 delay: 1000
#9 delay: 1000
...
*/

組み合わせスケジュールを Effect.repeat で使用すると、効果が最初のスケジュールのポリシー(再帰処理)に従って指定された回数の再発を完了するまで続きます。その後、2 番目のスケジュール(間隔のスケジュール)のポリシーに切り替わり、再発間の固定遅延で効果を繰り返します。

Jittering

jittered は、1 つのスケジュールを受け取り、その遅延がランダムに適用される同じタイプの新しいスケジュールを返すコンビネータです。

リソースが過負荷や競合のためにサービスを停止している場合、再試行とバッキングオフは役に立ちません。すべての失敗した API 呼び出しが同じポイントにバッキングオフされると、再度過負荷や競合が発生してしまいます。Jitter は、スケジュールの遅延にある程度のランダム性を追加します。これにより、意図せずに同期してサービスを停止してしまう事態を避けることができます。

調査によれば、Schedule.jittered(0.0, 1.0) は再試行に非常に適しています。

Delay.ts
// @include: Delay
// @filename: index.ts
// ---cut---
import { Schedule, Effect } from "effect";
import { log } from "./Delay";
const schedule = Schedule.jittered(Schedule.exponential("10 millis"));
const action = Effect.void;
log(action, schedule);
/*
Output:
delay: 0
#1 delay: 9.006765
#2 delay: 20.549507999999996
#3 delay: 45.86659000000001
#4 delay: 77.055037
#5 delay: 178.06722299999998
#6 delay: 376.056965
#7 delay: 728.732785
#8 delay: 1178.174953
#9 delay: 2331.4659370000004
...
*/

この例では、jittered コンビネータを使用して指数関数的スケジュールにジッターを適用しています。指数関数的スケジュールは、各繰り返しの間の遅延を指数的に増加させます。スケジュールにジッターを追加することで、遅延は特定の範囲内でランダムに調整されます。

フィルタリング

whileInputwhileOutput を使用して、スケジュールの入力や出力をフィルタリングできます。

Delay.ts
// @include: Delay
// @filename: index.ts
// ---cut---
import { Schedule, Effect } from "effect";
import { log } from "./Delay";
const schedule = Schedule.whileOutput(Schedule.recurs(5), (n) => n <= 2);
const action = Effect.void;
log(action, schedule);
/*
Output:
delay: 0
#1 delay: 0 < recurs
#2 delay: 0
#3 delay: 0
(end) < whileOutput
*/

この例では、Schedule.recurs(5) を使用して特定のアクションを最大 5 回繰り返すスケジュールを作成します。ただし、出力が 2 を超えた場合にフィルタリングするために、whileOutput コンビネータを適用します。その結果、値が 2 を超えると、スケジュールは出力を生成しなくなり、繰り返しが終了します。

修正

スケジュールの遅延を修正します。

Delay.ts
// @include: Delay
// @filename: index.ts
// ---cut---
import { Schedule, Effect } from "effect";
import { log } from "./Delay";
const schedule = Schedule.modifyDelay(
Schedule.spaced("1 second"),
(_) => "100 millis"
);
const action = Effect.void;
log(action, schedule);
/*
Output:
delay: 0
#1 delay: 100 < modifyDelay
#2 delay: 100
#3 delay: 100
#4 delay: 100
#5 delay: 100
#6 delay: 100
#7 delay: 100
#8 delay: 100
#9 delay: 100
...
*/

タッピング

スケジュールの入力や出力を効果的に処理する必要がある場合、tapInputtapOutput を使用できます。

これら 2 つの関数をログ目的で使用できます:

Delay.ts
// @include: Delay
// @filename: index.ts
// ---cut---
import { Schedule, Effect, Console } from "effect";
import { log } from "./Delay";
const schedule = Schedule.tapOutput(Schedule.recurs(2), (n) =>
Console.log(`repeating ${n}`)
);
const action = Effect.void;
log(action, schedule);
/*
Output:
delay: 0
repeating 0
#1 delay: 0
repeating 1
#2 delay: 0
repeating 2
*/