Skip to content

Releases: yandex/di

v0.18.0

Choose a tag to compare

@github-actions github-actions released this 24 Sep 04:33
c29c667

Three races that could leave a key two live values are closed, and a warm
resolution got several times faster. go doc -all against 0.17.2 is
unchanged in di and dislog; in dihttp one doc comment is reworded.
An upgrade can break a caller in one case: a resolution that fails now
leaves its route claimed, so a scope below the owner that tried to resolve a
key refuses a fallback registration of it afterwards (see Fixed). A
registration racing a resolution or a Wrap of the same key may now be
refused where it used to commit into an inconsistent state.

Changed

  • A warm Get no longer writes to the registration it reads. Every
    resolution stored the "resolved" mark again, on the cache line the value is
    read from, so cores resolving the same singleton kept invalidating each
    other.
  • A warm Get no longer allocates. It made two resolution-path nodes, one
    for the top-level call and one for the service, that nothing reads once
    the value is built. Together with the entry above, on an M3 Max: a warm
    Get went from 38 ns, 64 B and 2 allocations to 21 ns and none, and at
    eight cores from 56 ns to about 3 ns, or from 61 ns to about 6 ns through a
    child scope.
  • A Get through nested scopes no longer locks every scope between the
    resolving one and the owner each time. Recording that the key was handed
    down now stops at the first scope already recorded as handing it down from
    the same owner or one further out, so a request scope under a shared middle
    scope takes that scope's mutex once per key rather than on every Get:
    repeated warm Gets went from 181 ns at eight cores to about 8 ns
    (BenchmarkDI_Parallel_NestedGet).
  • A child scope is smaller and cheaper to open: 256 bytes rather than 288,
    and one allocation where there were three, since what only Run and
    Shutdown use, and the map only a Scoped binding needs, are made when
    first needed. Opening and stopping an empty child takes 6 allocations
    instead of 8.

Fixed

  • In digrpc/v0.2.2: Register no longer panics on every unary call to a
    method whose proto name is not its Go name (rpc get_user, served by
    GetUser); it looked the Go method up by the proto name. The generated
    handler now makes the typed call itself, with no reflection per call.
  • A Wrap racing another registration of the key it wraps can no longer
    leave two live values for the key or drop a registration unseen: an
    Override of the target could commit between Wrap finding it and
    marking it, and the wrapper went on serving a value built from a
    registration nothing else could reach; a registration, or another Wrap,
    landing in the wrapper's own scope meanwhile was silently displaced. Wrap
    now panics with a configuration error naming the registration it lost to
    (#52).
  • A Wrap over a registration that is marked Group() only afterwards is
    now rejected at the next resolution, as it already was when the
    registration was a group member before Wrap ran. It used to commit, so
    Get served the wrapper while All still returned the unwrapped member.
  • An Override committed concurrently with the first resolution of the
    registration it replaces can no longer let both through. The resolution
    could hand out the old value while the Override committed, leaving the
    key two live values; now one of them waits for the other, so either the
    resolution serves the new registration or the Override is rejected as
    already resolved. Found by review, not reported.
  • A registration in a scope between a resolving scope and the owner it
    resolves from, made while that resolution was building, no longer leaves
    the resolving scope with two values for the key: the first Get kept the
    ancestor's value and every later one got the new registration. The route
    is now claimed before the build, so such a registration is rejected as the
    scope having already resolved the key, and one committed before the claim
    reaches it is found and served instead. The same held beside and through a
    Wrap. A resolution that fails now leaves its route claimed as well, so
    a scope below the owner that tried to resolve the key refuses a fallback
    registration of it afterwards; register the fallback before resolving, or
    in the owner.

v0.17.2

Choose a tag to compare

@github-actions github-actions released this 23 Sep 13:33

The first release under golang.yandex/di. go doc -all against 0.17.1
changes one line per package — the import path — and nothing else: no
identifier, signature or behaviour differs in di, dihttp or dislog.
The upgrade breaks every caller all the same, because the module path
moved; the recipe below is the whole of the edit.

Added

  • digrpc, the gRPC counterpart of dihttp, as a separate module so the
    library keeps no dependency: digrpc.Interceptor opens a child scope per
    call holding a *digrpc.Call, digrpc.Module provides it, and
    Interceptor.Options() gives the server options. It is versioned on its
    own, as digrpc/vX.Y.Z.
  • digrpc.Register[H](srv, desc), in digrpc/v0.2.0, serves a generated
    service with an H resolved from each call's scope: the implementation is
    Scoped when it takes the call, is built after the server's interceptors
    have run, and a constructor's status error fails the call with that status.

Changed

  • The module path is now golang.yandex/di, and dihttp, dislog and
    digrpc move with it. This breaks every caller: change the go.mod
    requirement and the imports together, since the old and new paths are two
    modules as far as the toolchain is concerned and a build graph containing
    both would link two copies of the container. No identifier, signature or
    behaviour changed, so the edit is the path and nothing else:

    grep -rl '"github.com/floatdrop/di' --include='*.go' . \
      | xargs -r perl -pi -e 's{"github\.com/floatdrop/di(?=[/"])}{"golang.yandex/di}g'
    gofmt -w .   # the new path sorts elsewhere in a mixed import block
    go mod tidy  # drops the old requirement, adds the new one
    

    The rewrite is anchored on the opening quote and on the end of the path
    element, so it touches import paths only and leaves a
    github.com/floatdrop/dispatch alone. go mod tidy last, rather than a
    go get first, because dihttp, dislog and digrpc move in the same
    edit and it works out which of them a caller imports.

    A service's reported name carries its package path, so anything asserting on
    Explain, Graph, Modules or an Event.Service of a type declared in
    this module sees the new prefix.

  • The repository moved to https://github.com/yandex/di, and the guide
    with it, to https://yandex.github.io/di/. GitHub redirects the old
    location, so a remote, a clone or a link still works; nothing about the
    library does.

v0.17.1

Choose a tag to compare

@github-actions github-actions released this 19 Sep 13:37
533be1f

go doc -all against 0.17.0 adds dihttp.HandleFunc and changes nothing
else; nothing breaks.

Added

  • dihttp.HandleFunc(method) is dihttp.Handle as an http.HandlerFunc, for
    routers whose route methods take one rather than an http.Handler:
    r.Get("/users/{id}", dihttp.HandleFunc((*Users).Show)) on a chi router.

v0.17.0

Choose a tag to compare

@github-actions github-actions released this 18 Sep 10:06
2bb5f02

What a comparison with uber/fx turned up: a group or optional parameter can
be declared rather than resolved from a closure, Run bounds its start, a
binding carries one hook of each kind, and Explain reports what wiring
something late cost.

go doc -all against 0.16.2 adds Binding.Needs, Need, Optional, AllOf
and StartTimeout, and changes no signature and removes nothing, in di,
dihttp and dislog alike. Two behaviour changes can break a caller: a
binding that sets one hook of a kind twice now panics rather than keeping the
last one, and a Run whose start takes more than 15 seconds now fails and
rolls back, so a start that legitimately takes longer must say
StartTimeout.

Added

  • Binding.Needs, with Optional[T]() and AllOf[T](), declares the two
    parameters a constructor's signature cannot describe on its own:

    // func NewRouter(rs []Route, t *Tracer) *Router
    s.Wire[*Router](NewRouter).Needs(di.AllOf[Route](), di.Optional[*Tracer]())
    

    AllOf[T] fills a []T with the group for T, exactly as All[T]() does —
    read where the constructor runs, so a Scoped consumer sees the members its
    own scope adds, and the nil slice when the group is empty. Optional[T]
    fills a T if anything provides it and the zero value if nothing does, as
    Maybe[T]() does. Each Need is matched to its parameter by type, so
    parameter order stays the constructor's business; a Need that matches no
    parameter is rejected, as is a parameter described twice, and so is Needs
    on anything but a Wire or Wrap registration.

    Both were already possible through a Provide closure calling All or
    Maybe. What the markers add is that the dependency stays declared: the
    constructor remains a plain function, Validate checks it — walking a group
    member by member, and treating an unprovided optional as no failure —
    Explain draws it before anything is built, and Modules lists it. A
    closure took the whole constructor out of the checked graph for the sake of
    one parameter.

  • StartTimeout bounds Run's start phase as StopTimeout bounds its stop,
    and is 15 seconds by default: the context the OnStart hooks receive
    expires after it, the start ends between steps once it has, and the rollback
    runs. StartTimeout(0) takes the bound off, leaving the start bounded only
    by the context passed to Run, as it was before.
    A Run whose start legitimately takes longer than 15 seconds, such as one
    that migrates a database in an OnStart, must now say so. An OnStart hook
    that keeps its context for work outliving the start must take
    Scope.Context instead: under Run the one it is given is cancelled when
    the start ends.
    The deadline belongs to the phase and to nothing else. A constructor reads
    Scope.Context, which is still the context Run was called with, and a
    worker's context lasts as long as its service. It is checked between the
    steps Start drives, so it does not reach a service resolved from inside a
    start hook: that one is built and started there and then, on the scope's
    context, and a hook waiting on it is as unbounded as before.

  • Explain reports the values that were built on an answer that has since
    changed: missed by: *app.Router in root. Two questions get that treatment,
    Maybe[T], when T was registered afterwards, and All[T], when a member
    was. Both ask about the scope chain as it stands — a child scope may answer
    either differently — so registering late is not rejected, and it used to be
    silent instead. It is the answer to why a dependency or a group member that
    looks registered is not reaching the service that wanted it. Only an asker
    whose chain reached the scope the registration landed in is listed; one that
    asked from a sibling branch was never going to see it, and an ask made
    outside a constructor is not recorded at all, since no value was built on it.

Fixed

  • A second OnStart, OnDrain, OnStop or Go on one binding is rejected,
    naming the registration and the site of the second call. It used to assign
    over the first, so of two OnStop hooks only the last ran and nothing said
    that the other release had been dropped. A binding still carries one hook of
    each kind: combine them into one function, where the order within is the
    caller's.
  • Start no longer starts anything once its context's deadline has passed.
    Cancellation is unchanged: a cancelled context still finishes the start, so
    the rollback has everything to undo, which is how Run has always treated a
    signal during a slow start.

v0.16.2

Choose a tag to compare

@github-actions github-actions released this 13 Sep 17:54
a91c68e

A faster warm path and four lifecycle fixes. go doc -all against 0.16.1 is
unchanged in di, dihttp and dislog apart from the package doc's own
text; an upgrade cannot break a caller.

Changed

  • A warm resolution, of a service that is already built, no longer takes a
    mutex in the scope that owns it, and observers are called without one.
    Every Get of an application singleton used to serialise on the
    application scope, so it got slower per call as cores were added: 46 ns on
    one core and 418 ns on eight, on an M3 Max. It is now 44 ns and 55 ns, and
    a whole request scope at eight cores went from 2.3 µs to 1.4 µs.
    No API or behaviour changes; the new benchmarks/parallel_test.go records
    the shapes.
  • The cycle check a resolution makes before waiting on another goroutine's
    build no longer scans every waiting resolution in the container. Many
    resolutions arriving at one slow constructor made it quadratic under the
    container's one lock: 8192 of them took over 300 ms to settle and now take
    about 20 ms on an M3 Max (BenchmarkDI_Herd_8192 in
    benchmarks/parallel_test.go).

Fixed

  • A Wrap in a child scope no longer keeps the parent's registration from
    being overridden after the child has stopped. The parent's Override() was
    rejected as "wrapped at" a site in a scope that no longer existed. The same
    held after the child overrode its own wrapper. A wrapper in a live scope
    still guards what it wraps, including when a sibling scope that wrapped the
    same registration has stopped.
  • A service that was built and waiting for its start step when a drain swept
    past it now gets its OnDrain once it starts. The sweep decided it owed
    nothing, since it had not started, and made that final, so a Stop issued
    while Start was still starting earlier services released it with OnStop
    and no OnDrain. The drain phase also no longer misses a service built
    between its last sweep and the scope being marked stopped: the phase ends
    only when nothing in the scope's subtree has been built or started since
    that sweep began. So a Stop goes round again while anything below
    it is still being built or started, and under work that never quiets it
    returns when its context expires.

v0.16.1

Choose a tag to compare

@github-actions github-actions released this 12 Sep 12:40
14d3708

One fix. go doc -all against 0.16.0 is unchanged in di, dihttp and
dislog; an upgrade cannot break a caller.

Fixed

  • A worker registered with Go that panics is now a worker that failed: the
    panic becomes its error, Shutdown receives it, Run returns it, OnStop
    runs and the stop event carries it, as for a panicking hook. It was the one
    user function not called through the recovering wrapper, so its panic took
    the process down with no release and no event.

v0.16.0

Choose a tag to compare

@github-actions github-actions released this 10 Sep 15:12
fae9732

Binding.Worker is now Binding.Go, after errgroup.Group.Go and
sync.WaitGroup.Go, whose contract it has always had: run a function in a
goroutine the group tracks, cancel it when the group winds down, wait for it,
and let its error take the group down. go doc -all against 0.15.1 removes
Binding.Worker and adds Binding.Go with the same signature. An upgrade
breaks a caller that registers a worker
: rename the call from Worker to
Go, and nothing else changes.

Changed

  • Binding.Worker is renamed Binding.Go, with no alias left behind. The
    word "worker" still names what the method registers, in the docs and in the
    one error Stop reports about it, which now reads "worker did not return"
    rather than "Worker hook did not return".

v0.15.1

Choose a tag to compare

@github-actions github-actions released this 10 Sep 12:34
fc770df

A code-organisation release: the library is six files rather than one, and
the one observability gap the reorganisation turned up is closed. go doc -all against 0.15.0 changes doc comments only and no signature; dihttp and
dislog are untouched. An upgrade cannot break a caller.

Fixed

  • A panicking OnStart hook reports its EventStart, with the panic as
    Err, the way a panicking OnDrain or OnStop hook already reported
    theirs. Observers saw no start step at all for such a service; the failure
    still reached Start and Resolve as before.

v0.15.0

Choose a tag to compare

@floatdrop floatdrop released this 10 Sep 06:48

An application can say what it is doing. go doc -all against 0.14.0 adds one
field to Event, leaves every signature alone and leaves dihttp untouched;
dislog is a new package beside it. An upgrade cannot break a caller unless
it built an Event with an unkeyed composite literal.

Added

  • dislog, a new package that logs a scope's lifecycle events through
    log/slog: app.Observe(dislog.New(logger)) gives one line per constructor
    and per hook, so an application says what it is doing as it builds, starts,
    drains and stops. The event's kind is the message; the service, its scope,
    the module it came from and the duration are attributes. A step that failed
    logs at slog.LevelError with the error and the registration site, a step
    that succeeded at slog.LevelInfo or wherever dislog.Level puts it, and
    dislog.Site() logs the site every time. A service is named the way it is
    written in Go, with the import path lifted out into a pkg attribute. The
    package imports nothing beyond log/slog and di, so the library stays
    dependency-free and any handler will do; examples/ uses
    charmbracelet/log.
  • Event.Package, the import path of the type Event.Service names, so an
    observer can shorten a service name or group by its package without parsing
    one. It walks through pointers, since a pointer type is unnamed and carries
    no path of its own, and is empty for a key whose type is unnamed -- a
    []byte, a map[string]int -- because reflect already writes those with a
    short package name. An upgrade can break a caller only if it built an
    Event with an unkeyed composite literal, which go vet's composites check
    reports.

v0.14.0

Choose a tag to compare

@floatdrop floatdrop released this 07 Sep 14:44
d1e5c63

A module dependency report, derived from what registrations already carry.
go doc -all against 0.13.1 adds Scope.Modules and changes no signature;
an upgrade cannot break a caller.

Added

  • Scope.Modules() renders the modules registered into a scope and its
    ancestors: what each provides, what it needs and which module serves it,
    what it wraps, and which of its constructors are closures whose needs are
    known only when they run. A need only a resolving scope can provide is
    reported as owed, as Validate reports it, and a module's dependency on its
    own services is left out. It is derived from what registrations already
    carry, the module label and the parameters Wire declares, so there is
    nothing to declare twice. Keys are named by their package rather than their
    import path, to read beside the module labels.