Releases: yandex/di
Release list
v0.18.0
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
Getno 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
Getno 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
Getwent 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
Getthrough 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 everyGet:
repeated warmGets 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 onlyRunand
Shutdownuse, and the map only aScopedbinding needs, are made when
first needed. Opening and stopping an empty child takes 6 allocations
instead of 8.
Fixed
- In
digrpc/v0.2.2:Registerno 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
Wrapracing another registration of the key it wraps can no longer
leave two live values for the key or drop a registration unseen: an
Overrideof the target could commit betweenWrapfinding it and
marking it, and the wrapper went on serving a value built from a
registration nothing else could reach; a registration, or anotherWrap,
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
Wrapover a registration that is markedGroup()only afterwards is
now rejected at the next resolution, as it already was when the
registration was a group member beforeWrapran. It used to commit, so
Getserved the wrapper whileAllstill returned the unwrapped member. - An
Overridecommitted 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 theOverridecommitted, leaving the
key two live values; now one of them waits for the other, so either the
resolution serves the new registration or theOverrideis 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 firstGetkept 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
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 ofdihttp, as a separate module so the
library keeps no dependency:digrpc.Interceptoropens a child scope per
call holding a*digrpc.Call,digrpc.Moduleprovides it, and
Interceptor.Options()gives the server options. It is versioned on its
own, asdigrpc/vX.Y.Z.digrpc.Register[H](srv, desc), indigrpc/v0.2.0, serves a generated
service with anHresolved from each call's scope: the implementation is
Scopedwhen 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, anddihttp,dislogand
digrpcmove with it. This breaks every caller: change thego.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 oneThe 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/dispatchalone.go mod tidylast, rather than a
go getfirst, becausedihttp,disloganddigrpcmove 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,Modulesor anEvent.Serviceof 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
go doc -all against 0.17.0 adds dihttp.HandleFunc and changes nothing
else; nothing breaks.
Added
dihttp.HandleFunc(method)isdihttp.Handleas anhttp.HandlerFunc, for
routers whose route methods take one rather than anhttp.Handler:
r.Get("/users/{id}", dihttp.HandleFunc((*Users).Show))on a chi router.
v0.17.0
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, withOptional[T]()andAllOf[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[]Twith the group for T, exactly asAll[T]()does —
read where the constructor runs, so aScopedconsumer 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. EachNeedis matched to its parameter by type, so
parameter order stays the constructor's business; aNeedthat matches no
parameter is rejected, as is a parameter described twice, and so isNeeds
on anything but aWireorWrapregistration.Both were already possible through a
Provideclosure callingAllor
Maybe. What the markers add is that the dependency stays declared: the
constructor remains a plain function,Validatechecks it — walking a group
member by member, and treating an unprovided optional as no failure —
Explaindraws it before anything is built, andModuleslists it. A
closure took the whole constructor out of the checked graph for the sake of
one parameter. -
StartTimeoutboundsRun's start phase asStopTimeoutbounds its stop,
and is 15 seconds by default: the context theOnStarthooks 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 toRun, as it was before.
ARunwhose start legitimately takes longer than 15 seconds, such as one
that migrates a database in anOnStart, must now say so. AnOnStarthook
that keeps its context for work outliving the start must take
Scope.Contextinstead: underRunthe 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 contextRunwas called with, and a
worker's context lasts as long as its service. It is checked between the
stepsStartdrives, 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. -
Explainreports 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, andAll[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,OnStoporGoon one binding is rejected,
naming the registration and the site of the second call. It used to assign
over the first, so of twoOnStophooks 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. Startno 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 howRunhas always treated a
signal during a slow start.
v0.16.2
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.
EveryGetof 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 newbenchmarks/parallel_test.gorecords
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_8192in
benchmarks/parallel_test.go).
Fixed
- A
Wrapin a child scope no longer keeps the parent's registration from
being overridden after the child has stopped. The parent'sOverride()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 itsOnDrainonce it starts. The sweep decided it owed
nothing, since it had not started, and made that final, so aStopissued
whileStartwas still starting earlier services released it withOnStop
and noOnDrain. 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 aStopgoes 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
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
Gothat panics is now a worker that failed: the
panic becomes its error,Shutdownreceives it,Runreturns 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
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.Workeris renamedBinding.Go, with no alias left behind. The
word "worker" still names what the method registers, in the docs and in the
one errorStopreports about it, which now reads "worker did not return"
rather than "Worker hook did not return".
v0.15.1
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
OnStarthook reports itsEventStart, with the panic as
Err, the way a panickingOnDrainorOnStophook already reported
theirs. Observers saw no start step at all for such a service; the failure
still reachedStartandResolveas before.
v0.15.0
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 atslog.LevelErrorwith the error and the registration site, a step
that succeeded atslog.LevelInfoor whereverdislog.Levelputs 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 apkgattribute. The
package imports nothing beyondlog/sloganddi, so the library stays
dependency-free and any handler will do;examples/uses
charmbracelet/log.Event.Package, the import path of the typeEvent.Servicenames, 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, amap[string]int-- because reflect already writes those with a
short package name. An upgrade can break a caller only if it built an
Eventwith an unkeyed composite literal, whichgo vet's composites check
reports.
v0.14.0
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, asValidatereports 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 parametersWiredeclares, so there is
nothing to declare twice. Keys are named by their package rather than their
import path, to read beside the module labels.