Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/dynamic-idle-ttl-rcmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"effect": minor
---

RcMap: support dynamic `idleTimeToLive` values per key

The `idleTimeToLive` option can now be a function that receives the key and returns a duration, allowing different TTL values for different resources.

```ts
const map = yield* RcMap.make({
lookup: (key: string) => acquireResource(key),
idleTimeToLive: (key: string) => {
if (key.startsWith("premium:")) return Duration.minutes(10)
return Duration.minutes(1)
}
})
```
17 changes: 17 additions & 0 deletions .changeset/fast-shoes-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@effect/opentelemetry": patch
"effect": patch
---

Add logs to first propagated span, in the following case before this fix the log would not be added to the `p` span because `Effect.fn` adds a fake span for the purpose of adding a stack frame.

```ts
import { Effect } from "effect"

const f = Effect.fn(function* () {
yield* Effect.logWarning("FooBar")
return yield* Effect.fail("Oops")
})

const p = f().pipe(Effect.withSpan("p"))
```
5 changes: 5 additions & 0 deletions .changeset/many-maps-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/sql-pglite": minor
---

add @effect/sql-pglite package
5 changes: 5 additions & 0 deletions .changeset/violet-years-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": minor
---

Fix annotateCurrentSpan, add Effect.currentPropagatedSpan
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ The Effect monorepo is organized into multiple packages, each extending the core
| `@effect/sql-mssql` | An `@effect/sql` implementation using the mssql `tedious` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-mssql/README.md) |
| `@effect/sql-mysql2` | An `@effect/sql` implementation using the `mysql2` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-mysql2/README.md) |
| `@effect/sql-pg` | An `@effect/sql` implementation using the `postgres.js` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-pg/README.md) |
| `@effect/sql-pglite` | An `@effect/sql` implementation using `PGlite`, Postgres in the browser. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-pglite/README.md) |
| `@effect/sql-sqlite-bun` | An `@effect/sql` implementation using the `bun:sqlite` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-bun/README.md) |
| `@effect/sql-sqlite-do` | An `@effect/sql` implementation for Cloudflare Durable Objects sqlite storage. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-do/README.md) |
| `@effect/sql-sqlite-node` | An `@effect/sql` implementation using the `better-sqlite3` library. | [README](https://github.com/Effect-TS/effect/blob/main/packages/sql-sqlite-node/README.md) |
Expand Down
6 changes: 6 additions & 0 deletions packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12998,6 +12998,12 @@ export const annotateCurrentSpan: {
*/
export const currentSpan: Effect<Tracer.Span, Cause.NoSuchElementException> = effect.currentSpan

/**
* @since 3.20.0
* @category Tracing
*/
export const currentPropagatedSpan: Effect<Tracer.Span, Cause.NoSuchElementException> = effect.currentPropagatedSpan

/**
* @since 2.0.0
* @category Tracing
Expand Down
5 changes: 3 additions & 2 deletions packages/effect/src/RcMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export declare namespace RcMap {
*
* - `capacity`: The maximum number of resources that can be held in the map.
* - `idleTimeToLive`: When the reference count reaches zero, the resource will be released after this duration.
* Can be a static duration or a function that returns a duration based on the key.
*
* @since 3.5.0
* @category models
Expand Down Expand Up @@ -85,14 +86,14 @@ export const make: {
<K, A, E, R>(
options: {
readonly lookup: (key: K) => Effect.Effect<A, E, R>
readonly idleTimeToLive?: Duration.DurationInput | undefined
readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined
readonly capacity?: undefined
}
): Effect.Effect<RcMap<K, A, E>, never, Scope.Scope | R>
<K, A, E, R>(
options: {
readonly lookup: (key: K) => Effect.Effect<A, E, R>
readonly idleTimeToLive?: Duration.DurationInput | undefined
readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined
readonly capacity: number
}
): Effect.Effect<RcMap<K, A, E | Cause.ExceededCapacityException>, never, Scope.Scope | R>
Expand Down
25 changes: 18 additions & 7 deletions packages/effect/src/internal/core-effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1966,7 +1966,7 @@ export const annotateCurrentSpan: {
} = function(): Effect.Effect<void> {
const args = arguments
return ignore(core.flatMap(
currentSpan,
currentPropagatedSpan,
(span) =>
core.sync(() => {
if (typeof args[0] === "string") {
Expand Down Expand Up @@ -2041,6 +2041,16 @@ export const currentSpan: Effect.Effect<Tracer.Span, Cause.NoSuchElementExceptio
}
)

export const currentPropagatedSpan: Effect.Effect<Tracer.Span, Cause.NoSuchElementException> = core.flatMap(
core.context<never>(),
(context) => {
const span = filterDisablePropagation(Context.getOption(context, internalTracer.spanTag))
return span._tag === "Some" && span.value._tag === "Span"
? core.succeed(span.value)
: core.fail(new core.NoSuchElementException())
}
)

/* @internal */
export const linkSpans = dual<
(
Expand Down Expand Up @@ -2070,12 +2080,13 @@ export const linkSpans = dual<

const bigint0 = BigInt(0)

const filterDisablePropagation: (self: Option.Option<Tracer.AnySpan>) => Option.Option<Tracer.AnySpan> = Option.flatMap(
(span) =>
Context.get(span.context, internalTracer.DisablePropagation)
? span._tag === "Span" ? filterDisablePropagation(span.parent) : Option.none()
: Option.some(span)
)
export const filterDisablePropagation: (self: Option.Option<Tracer.AnySpan>) => Option.Option<Tracer.AnySpan> = Option
.flatMap(
(span) =>
Context.get(span.context, internalTracer.DisablePropagation)
? span._tag === "Span" ? filterDisablePropagation(span.parent) : Option.none()
: Option.some(span)
)

/** @internal */
export const unsafeMakeSpan = <XA, XE>(
Expand Down
6 changes: 4 additions & 2 deletions packages/effect/src/internal/fiberRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1517,13 +1517,15 @@ export const tracerLogger = globalValue(
logLevel,
message
}) => {
const span = Context.getOption(
const span = internalEffect.filterDisablePropagation(Context.getOption(
fiberRefs.getOrDefault(context, core.currentContext),
tracer.spanTag
)
))

if (span._tag === "None" || span.value._tag === "ExternalSpan") {
return
}

const clockService = Context.unsafeGet(
fiberRefs.getOrDefault(context, defaultServices.currentServices),
clock.clockTag
Expand Down
32 changes: 21 additions & 11 deletions packages/effect/src/internal/rcMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type * as Deferred from "../Deferred.js"
import * as Duration from "../Duration.js"
import type { Effect } from "../Effect.js"
import type { RuntimeFiber } from "../Fiber.js"
import { dual, identity } from "../Function.js"
import { constant, dual, flow, identity } from "../Function.js"
import * as MutableHashMap from "../MutableHashMap.js"
import { pipeArguments } from "../Pipeable.js"
import type * as RcMap from "../RcMap.js"
Expand Down Expand Up @@ -33,6 +33,7 @@ declare namespace State {
readonly deferred: Deferred.Deferred<A, E>
readonly scope: Scope.CloseableScope
readonly finalizer: Effect<void>
readonly idleTimeToLive: Duration.Duration
fiber: RuntimeFiber<void, never> | undefined
expiresAt: number
refCount: number
Expand All @@ -58,7 +59,7 @@ class RcMapImpl<K, A, E> implements RcMap.RcMap<K, A, E> {
readonly lookup: (key: K) => Effect<A, E, Scope.Scope>,
readonly context: Context.Context<never>,
readonly scope: Scope.Scope,
readonly idleTimeToLive: Duration.Duration | undefined,
readonly idleTimeToLive: ((key: K) => Duration.Duration) | undefined,
readonly capacity: number
) {
this[TypeId] = variance
Expand All @@ -73,27 +74,32 @@ class RcMapImpl<K, A, E> implements RcMap.RcMap<K, A, E> {
export const make: {
<K, A, E, R>(options: {
readonly lookup: (key: K) => Effect<A, E, R>
readonly idleTimeToLive?: Duration.DurationInput | undefined
readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined
readonly capacity?: undefined
}): Effect<RcMap.RcMap<K, A, E>, never, Scope.Scope | R>
<K, A, E, R>(options: {
readonly lookup: (key: K) => Effect<A, E, R>
readonly idleTimeToLive?: Duration.DurationInput | undefined
readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined
readonly capacity: number
}): Effect<RcMap.RcMap<K, A, E | Cause.ExceededCapacityException>, never, Scope.Scope | R>
} = <K, A, E, R>(options: {
readonly lookup: (key: K) => Effect<A, E, R>
readonly idleTimeToLive?: Duration.DurationInput | undefined
readonly idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined
readonly capacity?: number | undefined
}) =>
core.withFiberRuntime<RcMap.RcMap<K, A, E>, never, R | Scope.Scope>((fiber) => {
const context = fiber.getFiberRef(core.currentContext) as Context.Context<R | Scope.Scope>
const scope = Context.get(context, fiberRuntime.scopeTag)
const idleTimeToLive = options.idleTimeToLive === undefined
? undefined
: typeof options.idleTimeToLive === "function"
? flow(options.idleTimeToLive, Duration.decode)
: constant(Duration.decode(options.idleTimeToLive))
const self = new RcMapImpl<K, A, E>(
options.lookup as any,
context,
scope,
options.idleTimeToLive ? Duration.decode(options.idleTimeToLive) : undefined,
idleTimeToLive,
Math.max(options.capacity ?? Number.POSITIVE_INFINITY, 0)
)
return core.as(
Expand Down Expand Up @@ -169,10 +175,12 @@ const acquire = core.fnUntraced(function*<K, A, E>(self: RcMapImpl<K, A, E>, key
core.flatMap((exit) => core.deferredDone(deferred, exit)),
circular.forkIn(scope)
)
const idleTimeToLive = self.idleTimeToLive ? self.idleTimeToLive(key) : Duration.zero
const entry: State.Entry<A, E> = {
deferred,
scope,
finalizer: undefined as any,
idleTimeToLive,
fiber: undefined,
expiresAt: 0,
refCount: 1
Expand All @@ -192,19 +200,19 @@ const release = <K, A, E>(self: RcMapImpl<K, A, E>, key: K, entry: State.Entry<A
} else if (
self.state._tag === "Closed"
|| !MutableHashMap.has(self.state.map, key)
|| self.idleTimeToLive === undefined
|| Duration.isZero(entry.idleTimeToLive)
) {
if (self.state._tag === "Open") {
MutableHashMap.remove(self.state.map, key)
}
return core.scopeClose(entry.scope, core.exitVoid)
}

if (!Duration.isFinite(self.idleTimeToLive)) {
if (!Duration.isFinite(entry.idleTimeToLive)) {
return core.void
}

entry.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(self.idleTimeToLive)
entry.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(entry.idleTimeToLive)
if (entry.fiber) return core.void

return core.interruptibleMask(function loop(restore): Effect<void> {
Expand Down Expand Up @@ -276,10 +284,12 @@ export const touch: {
<K, A, E>(self_: RcMap.RcMap<K, A, E>, key: K) =>
coreEffect.clockWith((clock) => {
const self = self_ as RcMapImpl<K, A, E>
if (!self.idleTimeToLive || self.state._tag === "Closed") return core.void
if (self.state._tag === "Closed") return core.void
const o = MutableHashMap.get(self.state.map, key)
if (o._tag === "None") return core.void
o.value.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(self.idleTimeToLive)
const entry = o.value
if (Duration.isZero(entry.idleTimeToLive)) return core.void
entry.expiresAt = clock.unsafeCurrentTimeMillis() + Duration.toMillis(entry.idleTimeToLive)
return core.void
})
)
59 changes: 59 additions & 0 deletions packages/effect/test/RcMap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,63 @@ describe("RcMap", () => {

deepStrictEqual(yield* RcMap.keys(map), ["foo", "bar", "baz"])
}))

it.scoped("dynamic idleTimeToLive", () =>
Effect.gen(function*() {
const acquired: Array<string> = []
const released: Array<string> = []
const map = yield* RcMap.make({
lookup: (key: string) =>
Effect.acquireRelease(
Effect.sync(() => {
acquired.push(key)
return key
}),
() => Effect.sync(() => released.push(key))
),
idleTimeToLive: (key: string) => key.startsWith("short:") ? 500 : 2000
})

deepStrictEqual(acquired, [])

yield* Effect.scoped(RcMap.get(map, "short:a"))
yield* Effect.scoped(RcMap.get(map, "long:b"))
deepStrictEqual(acquired, ["short:a", "long:b"])
deepStrictEqual(released, [])

yield* TestClock.adjust(500)
deepStrictEqual(released, ["short:a"])

yield* TestClock.adjust(1500)
deepStrictEqual(released, ["short:a", "long:b"])
}))

it.scoped("dynamic idleTimeToLive with touch", () =>
Effect.gen(function*() {
const acquired: Array<string> = []
const released: Array<string> = []
const map = yield* RcMap.make({
lookup: (key: string) =>
Effect.acquireRelease(
Effect.sync(() => {
acquired.push(key)
return key
}),
() => Effect.sync(() => released.push(key))
),
idleTimeToLive: (key: string) => key.startsWith("short:") ? 500 : 2000
})

yield* Effect.scoped(RcMap.get(map, "short:a"))
deepStrictEqual(acquired, ["short:a"])
deepStrictEqual(released, [])

yield* TestClock.adjust(250)
yield* RcMap.touch(map, "short:a")
yield* TestClock.adjust(250)
deepStrictEqual(released, [])

yield* TestClock.adjust(250)
deepStrictEqual(released, ["short:a"])
}))
})
Loading