Skip to content

Commit

Permalink
enhance some english wordings
Browse files Browse the repository at this point in the history
  • Loading branch information
duongphuhiep committed Nov 8, 2024
1 parent 91edbcf commit 70a70c0
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 58 deletions.
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,20 +323,31 @@ Alias is also scoped by key. When you "Get" an alias with keys for eg: `ore.Get[

### Registration validation

It is recommended to build your container (which means register ALL the resolvers) only ONCE on application start.
Next, it is recommended to call `ore.Validate()`
Once finishing all your registrations, it is recommended to call `ore.Validate()`.

- either in a test which is automatically run on your CI/CD (option 1)
- or on application start, just after resolvers registration (option 2)

option 1 (run `ore.Validate` on test) is often a better choice.

`ore.Validate()` invokes ALL your registered resolvers, it panics when something gone wrong. The purpose of this function is to panic early when the Container is bad configured:
`ore.Validate()` invokes ALL your registered resolvers. The purpose is to panic early if your registrations were in bad shape:

- Missing depedency: you forgot to register certain resolvers.
- Circular dependency: A depends on B which depends on A.
- Lifetime misalignment: a longer lifetime service (eg. Singleton) depends on a shorter one (eg Transient).

### Registration recommendation

(1) You should call `ore.Validate()`

- either in a test which is automatically run on your CI/CD pipeline (option 1)
- or on application start, just after all the registrations (option 2)

option 1 (run `ore.Validate` on test) is usually a better choice.

(2) It is recommended to build your container (which means register ALL the resolvers) only ONCE on application start => Please don't call `ore.RegisterXX` all over the place.

(3) Keep the object creation function (a.k.a resolvers) simple. Their only responsibility should be **object creation**.

- they should not spawn new goroutine
- they should not open database connection
- they should not contain any "if" statement or other business logic

### Graceful application termination

On application termination, you want to call `Shutdown()` on all the "Singletons" objects which have been created during the application life time.
Expand All @@ -359,7 +370,7 @@ ore.RegisterEagerSingleton(&SomeService{}, "some_module") //*SomeService impleme
shutdowables := ore.GetResolvedSingletons[Shutdowner]()

//Now we can Shutdown() them all and gracefully terminate our application.
//The most recently created instance will be Shutdown() first
//The most recently invoked instance will be Shutdown() first
for _, instance := range disposables {
instance.Shutdown()
}
Expand All @@ -369,7 +380,7 @@ In resume, the `ore.GetResolvedSingletons[TInterface]()` function returns a list

- It returns only the instances which had been invoked (a.k.a resolved).
- All the implementations including "keyed" one will be returned.
- The returned instances are sorted by creation time (a.k.a the invocation order), the first one being the "most recently created" one.
- The returned instances are sorted by creation time (a.k.a the invocation order), the first one being the "most recently invoked" one.
- if "A" depends on "B", "C", Ore will make sure to return "B" and "C" first in the list so that they would be shutdowned before "A". However Ore won't guarantee the order of "B" and "C"

### Graceful context termination
Expand All @@ -394,7 +405,7 @@ go func() {
<-ctx.Done() // Wait for the context to be canceled
// Perform your cleanup tasks here
disposables := ore.GetResolvedScopedInstances[Disposer](ctx)
//The most recently created instance will be Dispose() first
//The most recently invoked instance will be Dispose() first
for _, d := range disposables {
_ = d.Dispose(ctx)
}
Expand All @@ -409,7 +420,7 @@ The `ore.GetResolvedScopedInstances[TInterface](context)` function returns a lis

- It returns only the instances which had been invoked (a.k.a resolved) during the context life time.
- All the implementations including "keyed" one will be returned.
- The returned instances are sorted by invocation order, the first one being the most "recently created" one.
- The returned instances are sorted by invocation order, the first one being the most "recently invoked" one.
- if "A" depends on "B", "C", Ore will make sure to return "B" and "C" first in the list so that they would be Disposed before "A". However Ore won't guarantee the order of "B" and "C"

## More Complex Example
Expand Down Expand Up @@ -480,6 +491,9 @@ BenchmarkGetList
BenchmarkGetList-20 1852132 637.0 ns/op
```

Checkout also [examples/benchperf/README.md](examples/benchperf/README.md)


# 👤 Contributors

![Contributors](https://contrib.rocks/image?repo=firasdarwish/ore)
Expand Down
19 changes: 12 additions & 7 deletions concrete.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import "time"

// concrete holds the resolved instance value and other metadata
type concrete struct {
// the value implementation
//the value implementation
value any
// the creation time
createdAt time.Time
// the lifetime of this concrete

//invocationTime is the time when the resolver had been invoked, it is different from the "creationTime"
//of the concrete. Eg: A calls B, then the invocationTime of A is before B, but the creationTime of A is after B
//(because B was created before A)
invocationTime time.Time

//the lifetime of this concrete
lifetime Lifetime
// the invocation deep level, the bigger the value, the deeper it was resolved in the dependency chain
// for example: A depends on B, B depends on C, C depends on D
// A will have invocationLevel = 1, B = 2, C = 3, D = 4

//the invocation deep level, the bigger the value, the deeper it was resolved in the dependency chain
//for example: A depends on B, B depends on C, C depends on D
//A will have invocationLevel = 1, B = 2, C = 3, D = 4
invocationLevel int
}
6 changes: 3 additions & 3 deletions examples/benchperf/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Benchmark comparison

This sample will compare Ore (current commit of Nov 2024) to [samber/do/v2 v2.0.0-beta.7](https://github.com/samber/do).
We registered the below dependency graphs to both Ore and SamberDo, then ask them to create the concrete A.
We registered the below dependency graphs to both Ore and SamberDo, then ask them to create the concrete `A`.

We will only benchmark the creation, not the registration. Because registration usually happens only once on application startup =>
not very interesting to benchmark.

## Data Model

- This data model has only 2 singletons `F` and `Gb` => they will be created only once
- Every other concrete are `Transient` => they will be created each time the container create a new `A`
- We don't test the "Scoped" lifetime in this excercise because SamberDo doesn't has equivalent support for it. The "Scoped" functionality of SamberDo means "Sub Module" rather than a lifetime.
- Other concretes are `Transient` => they will be created each time the container create a new `A` concrete.
- We don't test the "Scoped" lifetime in this excercise because SamberDo doesn't have equivalent support for it. [The "Scoped" functionality of SamberDo](https://do.samber.dev/docs/container/scope) means "Sub Module" rather than a lifetime.

```mermaid
flowchart TD
Expand Down
8 changes: 8 additions & 0 deletions get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ func TestGetKeyed(t *testing.T) {
}
}

func TestGetKeyedUnhashable(t *testing.T) {
RegisterLazyCreator(Singleton, &simpleCounter{}, "a")
_, _ = Get[someCounter](context.Background(), "a")

RegisterLazyCreator(Singleton, &simpleCounter{}, []string{"a", "b"})
_, _ = Get[someCounter](context.Background(), []string{"a", "b"})
}

func TestGetResolvedSingletons(t *testing.T) {
t.Run("When multiple lifetimes and keys are registered", func(t *testing.T) {
//Arrange
Expand Down
4 changes: 2 additions & 2 deletions getters.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ func GetResolvedScopedInstances[TInterface any](ctx context.Context) []TInterfac
func sortAndSelect[TInterface any](list []*concrete) []TInterface {
//sorting
sort.Slice(list, func(i, j int) bool {
return list[i].createdAt.After(list[j].createdAt) ||
(list[i].createdAt == list[j].createdAt &&
return list[i].invocationTime.After(list[j].invocationTime) ||
(list[i].invocationTime == list[j].invocationTime &&
list[i].invocationLevel > list[j].invocationLevel)
})

Expand Down
4 changes: 2 additions & 2 deletions ore.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ var (

//contextKeysRepositoryID is a special context key. The value of this key is the collection of other context keys stored in the context.
contextKeysRepositoryID specialContextKey = "The context keys repository"
//contextKeyResolversChain is a special context key. The value of this key is the [ResolversChain].
contextKeyResolversChain specialContextKey = "Dependencies chain"
//contextKeyResolversStack is a special context key. The value of this key is the [ResolversStack].
contextKeyResolversStack specialContextKey = "Dependencies stack"
)

type contextKeysRepository = []contextKey
Expand Down
6 changes: 3 additions & 3 deletions registrars.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ func RegisterEagerSingleton[T comparable](impl T, key ...KeyStringer) {
lifetime: Singleton,
},
singletonConcrete: &concrete{
value: impl,
lifetime: Singleton,
createdAt: time.Now(),
value: impl,
lifetime: Singleton,
invocationTime: time.Now(),
},
}
appendToContainer[T](e, key)
Expand Down
60 changes: 31 additions & 29 deletions serviceResolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,29 @@ type serviceResolverImpl[T any] struct {
singletonConcrete *concrete
}

// resolversChain is a linkedList[resolverMetadata], describing a dependencies chain which a resolver has to invoke other resolvers to resolve its dependencies.
// Before a resolver creates a new concrete value it would be registered to the resolversChain.
// Once the concrete is resolved (with help of other resolvers), then it would be removed from the chain.
// resolversStack is a stack of [resolverMetadata], similar to a call stack describing How a resolver has
// to call other resolvers to resolve its dependencies.
// Before a resolver creates a new concrete value it would be registered (pushed) to the stack.
// Once the concrete is resolved (with help of other resolvers), then it would be removed (poped) from the stack.
//
// While a Resolver forms a tree with other dependent resolvers.
//
// Example:
//
// A calls B and C; B calls D; C calls E.
//
// then resolversChain is a "path" in the tree from the root to one of the bottom.
// then resolversStack holds a "path" in the tree from the root to one of the bottom.
//
// Example:
//
// A -> B -> D or A -> C -> E
//
// The resolversChain is stored in the context. Analyze the chain will help to
// The resolversStack is stored in the context. Analyze the stack will help to
//
// - (1) detect the invocation level
// - (2) detect cyclic dependencies
// - (3) detect lifetime misalignment (when a service of longer lifetime depends on a service of shorter lifetime)
type resolversChain = *list.List
type resolversStack = *list.List

// make sure that the `serviceResolverImpl` struct implements the `serviceResolver` interface
var _ serviceResolver = serviceResolverImpl[any]{}
Expand All @@ -70,25 +71,25 @@ func (this serviceResolverImpl[T]) resolveService(ctx context.Context) (*concret
}
}

// this resolver is about to create a new concrete value, we have to put it to the resolversChain until the creation done
// this resolver is about to create a new concrete value, we have to put it to the resolversStack until the creation done

// get the current currentChain from the context
var currentChain resolversChain
// get the current currentStack from the context
var currentStack resolversStack
var marker *list.Element
if !DisableValidation {
untypedCurrentChain := ctx.Value(contextKeyResolversChain)
if untypedCurrentChain == nil {
currentChain = list.New()
ctx = context.WithValue(ctx, contextKeyResolversChain, currentChain)
untypedCurrentStack := ctx.Value(contextKeyResolversStack)
if untypedCurrentStack == nil {
currentStack = list.New()
ctx = context.WithValue(ctx, contextKeyResolversStack, currentStack)
} else {
currentChain = untypedCurrentChain.(resolversChain)
currentStack = untypedCurrentStack.(resolversStack)
}

// push this newest resolver to the resolversChain
marker = appendResolver(currentChain, this.resolverMetadata)
// push the current resolver to the resolversStack
marker = pushToStack(currentStack, this.resolverMetadata)
}
var concreteValue T
createdAt := time.Now()
invocationTime := time.Now()

// first, try make concrete implementation from `anonymousInitializer`
// if nil, try the concrete implementation `Creator`
Expand All @@ -100,16 +101,17 @@ func (this serviceResolverImpl[T]) resolveService(ctx context.Context) (*concret

invocationLevel := 0
if !DisableValidation {
invocationLevel = currentChain.Len()
invocationLevel = currentStack.Len()

// the concreteValue is created, we must to remove it from the resolversChain so that downstream resolvers (meaning the future resolvers) won't link to it
currentChain.Remove(marker)
//the concreteValue is created, we must to pop the current resolvers from the stack
//so that future resolvers won't link to it
currentStack.Remove(marker)
}

con := &concrete{
value: concreteValue,
lifetime: this.lifetime,
createdAt: createdAt,
invocationTime: invocationTime,
invocationLevel: invocationLevel,
}

Expand All @@ -130,25 +132,25 @@ func (this serviceResolverImpl[T]) resolveService(ctx context.Context) (*concret
return con, ctx
}

// appendToResolversChain push the given resolver to the Back of the ResolversChain.
// pushToStack appends the given resolver to the Back of the given resolversStack.
// `marker.previous` refers to the calling (parent) resolver
func appendResolver(chain resolversChain, currentResolver resolverMetadata) (marker *list.Element) {
if chain.Len() != 0 {
func pushToStack(stack resolversStack, currentResolver resolverMetadata) (marker *list.Element) {
if stack.Len() != 0 {
//detect lifetime misalignment
lastElem := chain.Back()
lastElem := stack.Back()
lastResolver := lastElem.Value.(resolverMetadata)
if lastResolver.lifetime > currentResolver.lifetime {
panic(lifetimeMisalignment(lastResolver, currentResolver))
}

//detect cyclic dependencies
for e := chain.Back(); e != nil; e = e.Prev() {
for e := stack.Back(); e != nil; e = e.Prev() {
if e.Value.(resolverMetadata).id == currentResolver.id {
panic(cyclicDependency(currentResolver))
}
}
}
marker = chain.PushBack(currentResolver) // `marker.previous` refers to the calling (parent) resolver
marker = stack.PushBack(currentResolver) // `marker.previous` refers to the calling (parent) resolver
return marker
}

Expand All @@ -173,9 +175,9 @@ func (this resolverMetadata) String() string {
return fmt.Sprintf("Resolver(%s, type={%s}, key='%s')", this.lifetime, getUnderlyingTypeName(this.id.pointerTypeName), this.id.oreKey)
}

// func toString(resolversChain resolversChain) string {
// func toString(resolversStack resolversStack) string {
// var sb string
// for e := resolversChain.Front(); e != nil; e = e.Next() {
// for e := resolversStack.Front(); e != nil; e = e.Next() {
// sb = fmt.Sprintf("%s%s\n", sb, e.Value.(resolverMetadata).String())
// }
// return sb
Expand Down

0 comments on commit 70a70c0

Please sign in to comment.