Skip to content

Commit

Permalink
some doc
Browse files Browse the repository at this point in the history
  • Loading branch information
Dk committed May 28, 2024
1 parent 630096d commit a313284
Show file tree
Hide file tree
Showing 19 changed files with 400 additions and 196 deletions.
2 changes: 2 additions & 0 deletions ContextForTest.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ func (c ContextForTest[D]) GetCtx() context.Context {
func (c *ContextForTest[D]) servedWeb() {
}

func (c *ContextForTest[D]) served() {}

type PipelineContextForTest[D IPipelineCookware[M], M IPipelineModel] struct {
ContextForTest[D]
DummyTx IDbTx
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Kitchen Framework

A golang framework for building progressive backend services.

![](./docs/asset/cover.jpeg)

## Introduction

Kitchen is a framework designed for building progressive, scalable services.
Expand Down
6 changes: 6 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ func (c *Context[D]) servedWeb() {
}
}

func (c *Context[D]) served() {
if len(c.session) == 1 {
c.dish.menu().cookwareRecycle(c.cookware)
}
}

type PipelineContext[D IPipelineCookware[M], M IPipelineModel] struct {
Context[D]
tx IDbTx
Expand Down
4 changes: 2 additions & 2 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

func TestWebContextNormal(t *testing.T) {
dummyWebCtx, cancel := context.WithCancel(context.Background())
webCtx := NewWebContext(context.WithValue(dummyWebCtx, "test1", 1))
webCtx := NewWebContext(context.WithValue(dummyWebCtx, "test1", 1), nil, nil)
ctx := context.WithValue(webCtx, "test2", 2)
go func() {
done := ctx.Done()
Expand All @@ -28,7 +28,7 @@ func TestWebContextNormal(t *testing.T) {

func TestWebContextAbnormal(t *testing.T) {
dummyWebCtx, cancel := context.WithCancel(context.Background())
ctx := NewWebContext(context.WithValue(dummyWebCtx, "test", 1))
ctx := NewWebContext(context.WithValue(dummyWebCtx, "test", 1), nil, nil)
go func() {
done := ctx.Done()
<-done
Expand Down
6 changes: 1 addition & 5 deletions cookbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ type cookbook[D ICookware, I any, O any] struct {
fullName string
isTraceable bool
isInheritableCookware bool
isWebWrapperCookware bool
}

var (
Expand Down Expand Up @@ -79,10 +78,6 @@ func (r *cookbook[D, I, O]) inherit(ev ...iCookbook[D]) {
r.inherited = append(r.inherited, ev...)
}

func (r cookbook[D, I, O]) emitAfterExec(ctx IContext[D], input, output any, err error) {
r.emitAfterCook(ctx, input, output, err)
}

func (r cookbook[D, I, O]) emitAfterCook(ctx IContext[D], input, output any, err error) {
if l := len(r.asyncAfterListenHandlers); l+len(r.afterListenHandlers) != 0 {
if l != 0 {
Expand Down Expand Up @@ -152,6 +147,7 @@ func (r cookbook[D, I, O]) start(ctx IContext[D], input I, panicRecover bool) (s
} else {
fmt.Printf("panicRecover from panic: \n%v\n%s", r, string(debug.Stack()))
}
sess.finish(nil, fmt.Errorf("panic: %v", rec))
}
}()
}
Expand Down
36 changes: 20 additions & 16 deletions dish.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
type Dish[D ICookware, I any, O any] struct {
cookbook[D, I, O]
sets []iSet[D]
menu iMenu[D]
_menu iMenu[D]
name string
cooker DishCooker[D, I, O]
rawCooker DishCooker[D, I, O]
Expand Down Expand Up @@ -62,16 +62,16 @@ func (a *Dish[D, I, O]) init(parent iCookbook[D], action iDish[D], name string,
setNames = append([]string{g.Name()}, setNames...)
}
a.inherit(set.menu())
a.menu = set.menu()
a._menu = set.menu()
if len(setNames) != 0 {
a.fullName = fmt.Sprintf("%s.%s.%s", a.menu.Name(), strings.Join(setNames, "."), a.name)
a.fullName = fmt.Sprintf("%s.%s.%s", a._menu.Name(), strings.Join(setNames, "."), a.name)
} else {
a.fullName = fmt.Sprintf("%s.%s", a.menu.Name(), a.name)
a.fullName = fmt.Sprintf("%s.%s", a._menu.Name(), a.name)
}
} else {
a.inherit(parent)
a.menu = any(parent).(iMenu[D])
a.fullName = fmt.Sprintf("%s.%s", a.menu.Name(), a.name)
a._menu = any(parent).(iMenu[D])
a.fullName = fmt.Sprintf("%s.%s", a._menu.Name(), a.name)
}
var (
input I
Expand All @@ -87,8 +87,8 @@ func (a *Dish[D, I, O]) init(parent iCookbook[D], action iDish[D], name string,
} else {
a.marshalOutput = newJsonUnmarshaler(output)
}
a.id = uint32(a.menu.pushDish(action))
a.isTraceable = a.menu.isTraceableDep()
a.id = uint32(a._menu.pushDish(action))
a.isTraceable = a._menu.isTraceableDep()
var (
iType = reflect.TypeOf((*I)(nil)).Elem()
)
Expand Down Expand Up @@ -193,8 +193,12 @@ func (a Dish[D, I, O]) Tags() reflect.StructTag {
return a.fieldTags
}

func (a Dish[D, I, O]) menu() iMenu[D] {
return a._menu
}

func (a Dish[D, I, O]) Menu() IMenu {
return a.menu
return a._menu
}

func (a Dish[D, I, O]) Sets() []ISet {
Expand Down Expand Up @@ -274,8 +278,8 @@ func (a *Dish[D, I, O]) SetCooker(cooker DishCooker[D, I, O]) *Dish[D, I, O] {
a.asyncChan = nil
}
a.rawCooker = cooker
if a.menu.Manager() != nil {
var mgr = a.menu.Manager()
if a._menu.Manager() != nil {
var mgr = a._menu.Manager()
a.cooker = func(ctx IContext[D], input I) (output O, err error) {
var (
handler func(ctx context.Context, input []byte) (output []byte, err error)
Expand Down Expand Up @@ -313,16 +317,16 @@ func (a *Dish[D, I, O]) PanicRecover(recover bool) *Dish[D, I, O] {
}

func (a Dish[D, I, O]) Cookware() ICookware {
return a.menu.Cookware()
return a._menu.Cookware()
}

func (a Dish[D, I, O]) cookware() D {
d := a.menu.cookware()
d := a._menu.cookware()
return d
}

func (a Dish[D, I, O]) Dependency() D {
d := a.menu.cookware()
d := a._menu.cookware()
return d
}

Expand Down Expand Up @@ -432,7 +436,7 @@ func (a *Dish[D, I, O]) newCtx(ctx context.Context, cookware ...D) *Context[D] {
if c, ok = ctx.(*Context[D]); ok {
return c
}
c = &Context[D]{Context: ctx, menu: a.menu, sets: a.sets, dish: a}
c = &Context[D]{Context: ctx, menu: a._menu, sets: a.sets, dish: a}
var (
cw D
)
Expand All @@ -447,7 +451,7 @@ func (a *Dish[D, I, O]) newCtx(ctx context.Context, cookware ...D) *Context[D] {
}
}
if c.inherited, ok = ctx.(IContextWithSession); ok {
if a.menu.isInheritableDep() {
if a._menu.isInheritableDep() {
c.cookware = any(cw).(ICookwareInheritable).Inherit(c.inherited.RawCookware()).(D)
} else {
c.cookware = cw
Expand Down
Binary file added docs/asset/cover.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/asset/intro_chart1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/asset/intro_cmp1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/asset/intro_ms1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Kitchen

A golang framework for building progressive backend services.

![](./asset/cover.jpeg)

- [Introduction](./intro.md)


- [Repo](https://github.com/go-preform/kitchen)
143 changes: 143 additions & 0 deletions docs/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# My answer to microservices vs monoliths


As a backend developer and system architect, the biggest choice I have to make is how to balance the trade-offs
of being simple efficient and scalable: keep the system in a monolithic architecture or split it into microservices.


### Microservices

It's usually considered as the advanced choice by most developers. Indeed, yes, it's the ultimate solution.
And the team can enjoy some benefits other than performance and scalability, like independent deployment, minimized downtime,
flexible technology stack, etc. I can understand why developers are so hard on with it. However, the trade-offs are also
obvious: complexity, and cost. It's just too hard! Every time when developer ask me to use microservices architecture,
I would question about if we can do it right. To be honest even I am not that confident, especially after certain updates.
Please take a look at this if you question about it.

[![microservice by KRAZAM](./asset/intro_ms1.png)](https://www.youtube.com/watch?v=y8OnoxKotPQ)

I can't stop laughing everytime when I watch it, but it's so true. You may read the comments and also [ThePrimeTime's react](https://www.youtube.com/watch?v=s-vJcOfrvi0) to it.
That's not hard to understand right? Just consider the spaghetti code you've ever written and imagine they are now in network calls, absolutely nightmare.


### Monolith

I love monoliths, if we can't foresee a million users or thousands of concurrency, who needs microservices?
It's way easier to develop, deploy and maintain. No need of to worry about service discovery, inter-service portal,
network drops, latency, blocking, timeout, etc. And as hardware performance and CI/CD technique improves,
a monolith on 100+ cores server with proper pipelines can well support a decent scaled system with minimal downtime.
When a single monolith can't handle alone, it can still scale itself horizontally, I would say >90% of the case it's sufficient.

### Modular Monolith

After considering all the pros and cons, developers nowadays seems not that into microservices. Big tech like Amazon, Google
are moving some of their less intensive products back to monoliths. But of course, that are big companies, and they always
prepare for growing fast and big in foreseeable future. So modular monoliths are their choice. For short, it's some microservices
running in a same process but still calling each other through network APIs. It's obviously easier than microservices and yet
more scalable than ordinary monolith. But from my understanding, it's not that ideal, just like the graph below.

![](./asset/intro_cmp1.png)

The overhead of coding is still there no matter use Restful or gRPC or MQ. It requires a lot more efforts to manage the communication.
And obviously the performance overhead is there since it use network calls regardless of the fact that they are in the same process.
Even if they can overcome that with some helper like fake client/server, it still increases the cost of implementation/splitting.
Moreover, in case if it needs to split one day, the cost is still not that low especially when taking deployment into consideration.

## My attempt: Kitchen

After all of these, I've come up with my own solution: Kitchen. I've set some goals for it:

1. Minimal development overhead
1. Minimal performance overhead
1. Seamless scaling
1. Manageable call stack
1. Manageable dependencies

### How it works

The core concept of Kitchen is to create placeholders for all major functions. These placeholders define the input, output, and dependencies required for execution. Function bodies can then be assigned to these placeholders.

At runtime, functions are invoked from the placeholders. This allows for the integration of additional logic such as logging, tracing, metrics, callbacks, and more into the function calls without messing up the code.

And most importantly, since execution is called via placeholders, it is not necessarily executed in local. Let's see how it hit my goals!

### Minimal development overhead

The major overhead of using kitchen is to predefine the placeholders before coding the actual logic.

```go
type SomeTaskes struct {
kitchen.MenuBase[*SomeTaskes, *SomeDependecy]
Task1 kitchen.Dish[*SomeDependecy, *Input1, *Output1]
Task2 kitchen.Dish[*SomeDependecy, *Input2, *Output2]
}

someTaskes := kitchen.InitMen(&SomeTaskes{}, &SomeDependecy{})
someTaskes.Task1.SetCooker(func(dep *SomeDependecy, input *Input1, output *Output1) {
// do something
})
someTaskes.Task2.SetCooker(doSomethingFn)

output1, err := someTaskes.Task1.Cook(input1)
```

It takes some time of course but compare to drafting APIs it's way easier. And Kitchen provides convenient plugins for turning the placeholders into web APIs, generating OpenAPI schema, and gRPC adapter etc.

### Minimal performance overhead

To be a framework for real battles, I've try put performance into first place, minimize the use of reflect or map.
It makes the local call overhead is as low as <400ns, and <10000ns for network calls.

The network helper is based on ZeroMQ, which is a high-throughput, low-latency networking library.
It's way faster than HTTP and gRPC, and I've further improved it by implementing a 2 way data link.
Every separated node will have a pool of tcp connections for sending request, but unlike traditional
network calls, they don't wait / block.And every node will have a listening port handling with single
goroutine, then requests will pass to the corresponding goroutine through channels. After the request
is processed, the response will be sent back through the request connections targeted to the requester.
This two-way data flow enabled a true async call, which is optimal for microservices use cases.


![](./asset/intro_chart1.png)

Note that the network helper is interchangeable, you can implement other helper to suit the requirement, for example
a MQ adapter is ideal for services require the highest level of durability.

### Seamless scaling

The network helper is designed to be configurable in runtime, service can easily replicate themselves into horizontal monoliths.
Or they can even toggle feature scopes in runtime to become a single responsibility services by CI/CD pipeline or even APIs without restarting.
The core logic can stay unchanged as long as the dependencies are managed properly and the placeholders are properly defined.

### Manageable call stack

Metric, logger, tracer, etc are easy to implement, you can easily add them to the placeholders. The logger, tracer plugins
are provided by default, and additional plugins can be added by either tracer interface or as afterCook callback.

It also provided concurrent limit control, it's helpful to prevent part of service drain all the resource. Unfortunately,
the memory profile lib provided by golang messed up the generic type, so I can't provide a memory limit control.

Consider the placeholder as an internal URI, you can add all the things you previously did with middlewares to it.

### Manageable dependencies

It's important to keep dependencies manageable as we aim at split the modules one day.
It supports both singleton or factory / sync pool pattern. And they are injected into
the function body inside the context parameter.

I am planning to add a dependency initialize and depose interface to let Kitchen prepare it for the corresponding functions are enabled and release resources when they are disabled.

And it can perform some extra logic like server middlewares by implementing certain interfaces, for example, IWebParsableInput for parsing web request into input
and handle session, ACL, etc.

### Conclusion

As a system architect, I am always dreaming of a perfect solution that can balance all the trade-offs, something that's easy to develop, deploy, and maintain, yet scalable and efficient.
One set of code suit all the scenarios with minimal modification, progressively scale as business growth.

Kitchen is my attempt to achieve this. I will keep improving it and hope it can help you as well. Any feedback is welcome.


![](./asset/cover.jpeg)


Thank you for reading.
Loading

0 comments on commit a313284

Please sign in to comment.