Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

📝 [Proposal]: Support for Application State Management #3077

Open
3 tasks done
gaby opened this issue Jul 17, 2024 · 7 comments
Open
3 tasks done

📝 [Proposal]: Support for Application State Management #3077

gaby opened this issue Jul 17, 2024 · 7 comments

Comments

@gaby
Copy link
Member

gaby commented Jul 17, 2024

Feature Proposal Description

This proposal aims to introduce Application State Management feature in Fiber to enable the sharing of stateful data across middleware and request handlers efficiently. This is a feature supported in other language frameworks for example: Starlette and FastAPI.

  • Introduce App State Management: Implement a state management system that allows storing and accessing application-wide state.
  • Middleware and Handler Integration: Ensure seamless integration with existing middleware and request handlers.
  • Ease of Use: Provide a simple and intuitive API for developers to interact with the application state.
  • Performance: Ensure that the state management system does not compromise the performance of Fiber.

Sources:

Alignment with Express API

In fiber these proposal is similar to ctx.Locals but applies to the whole application. Resources stored in the state can be accessed by multiple handlers, middlewares, etc.

In Express.js it's similar to app.locals, but in express the values can only be used when rendering templates.

HTTP RFC Standards Compliance

N/a

API Stability

API Methods

  • Set: Set a key-value pair in the application state.
    • What do we do if the Key already exists?
    • Do we add MustSet, to panic if key already exists?
  • Get: Retrieve a value by key from the application state.
  • MustGet: Retrieve a value by key from the application state, panic if not found.
  • Delete: Remove a key-value pair from the application state.
    • This probably requires a mutex.
  • Exists: Check if a key exists in the application state.

We could also add specific Get functions for specific common types: int, string, float, etc. For example, for getting a string from the app.State:

func (s *State) GetString(key string) (string, bool) {
	value, exists := s.state[key]
	if !exists {
		return "", false
	}
	str, ok := value.(string)
	return str, ok
}

It would be the responsibility of the developer to call to appropriate function.

Feature Examples

func main() {
    app := fiber.New()

    // Create and set the database client
    connString := "postgres://username:password@localhost:5432/database_name"
    conn, _ := pgx.Connect(context.Background(), connString)
    defer conn.Close(context.Background())

    // Store DB client in App State
    app.State.Set("db", conn)

    app.Get("/", func(c *fiber.Ctx) error {
        // Access the database connection from the AppState
        db, exists := app.State.Get("db")
        if !exists {
            return c.Status(fiber.StatusInternalServerError).SendString("Database connection not found")
        }

        // Use the database connection
        pgxConn := db.(*pgx.Conn)
        var greeting string
        err := pgxConn.QueryRow(context.Background(), "SELECT 'Hello, world!'").Scan(&greeting)
        if err != nil {
            return c.Status(fiber.StatusInternalServerError).SendString("Failed to execute query")
        }
        return c.SendString(greeting)
    })

    app.Listen(":3000")
}

Checklist:

  • I agree to follow Fiber's Code of Conduct.
  • I have searched for existing issues that describe my proposal before opening this one.
  • I understand that a proposal that does not meet these guidelines may be closed without explanation.
@brunodmartins
Copy link
Contributor

On the applications I currently use Fiber in production, we create a whole set of application components using a single pointer of structs, and perform the Dependency Injection at the bootstrap of the application to provide this "Application State". I will give an example on how we perform it today:

func main() {
	app := fiber.New()
	service := UserService{}
	handler := UserHandler{
		service: service,
	}
	app.Get("/users/:id", handler.GetUser)
	app.Listen(":3000")
}

type User struct{}

type UserService struct {
}

func (*UserService) GetUser(id string) (*User, error) {
	return nil, nil
}

type UserHandler struct{
	service UserService
}

func (handler *UserHandler) GetUser(ctx fiber.Ctx) error {
	_, _ = handler.service.GetUser(ctx.Get(":id"))
	return nil
}

In any case, such solution (Fiber's Application State) could help us improve the solution we have today.

@ReneWerner87 ReneWerner87 added this to v3 Jul 20, 2024
@ReneWerner87 ReneWerner87 added this to the v3 milestone Jul 20, 2024
@mdelapenya
Copy link

Hi from the Testcontainers for Go community 👋 ! Core maintainer here 🙋

We have a workshop for learning the fundamentals of Testcontainers in the Go language, and we have what we call local development mode, in which we could use the build to start the runtime dependencies (databases, queues, cloud services...) selectively and only when certain build tags (e.g. -tags=dev) are passed to the Go toolchain. Then, the dependencies in those "protected-by-build-tags" file will be only added to the final binary if and only if the dev mode build tags are used, not ending up in the production binary.

I think this approach could be combined with the built-in capabilities in GoFiber, to start these dependencies in a hook only when the build adds the right build tags.

You can check it here: https://github.com/testcontainers/workshop-go/blob/main/step-4-dev-mode-with-testcontainers.md

What are your thoughts on this?

@gaby
Copy link
Member Author

gaby commented Aug 29, 2024

@mdelapenya Does this require adding testcontainers as a dependency in Fiber go.mod? Or is there a way to keep it separate.

@mdelapenya
Copy link

I'm afraid I'm not an expert in the Fiber ecosistem yet, but I'd say that, if we are able to create Go modules for them so that only those using the "hooks" (or whatever implementation we use) will receive the dependencies. In the end, they will be dev-time dependencies, so the production build won't receive them.

@mdelapenya
Copy link

mdelapenya commented Aug 30, 2024

@gaby I'd expect Fiber providing a "RuntimeDependency" interface with at least the Start and Stop methods, so that this hook could control the lifecycle of those startable dependencies. An implementer of that interface (e.g. a hook for testcontainers-go module) would contribute runtime dependencies (databases, queues, cloud services...) to some state in the framework, so that the framework can handle their lifecycles (start & stop when needed).

Something like:

package fiber

import "context"

type RuntimeDependency interface {
	Start(context.Context) error
	Terminate(context.Context) error
}

// hasDependencies Checks if there are any dependency for the current application.
func (app *App) hasDependencies() bool {
	return len(app.runtimeDependencies) > 0
}

// startDependencies Handles the startup process of dependencies for the current application.
// Iterates over all dependencies and starts them, panics if any error occurs.
func (app *App) startDependencies() {
	if app.hasDependencies() {
		for _, dep := range app.runtimeDependencies {
			err := dep.Start(app.newCtx().Context())
			if err != nil {
				panic(err)
			}
		}
	}
}

// shutdownDependencies Handles the shutdown process of dependencies for the current application.
// Iterates over all dependencies and terminates them, panics if any error occurs.
func (app *App) shutdownDependencies() {
	if app.hasDependencies() {
		for _, dep := range app.runtimeDependencies {
			err := dep.Terminate(app.newCtx().Context())
			if err != nil {
				panic(err)
			}
		}
	}
}

And in app.go:

// App denotes the Fiber application.
type App struct {
	...
	// runtimeDependencies is a list of dependencies that are used by the app (e.g. databases, caches, etc.)
	runtimeDependencies []RuntimeDependency
}

Calling app.shutdownDependencies() and app.startDependencies() where it better fits.

Quick and dirty idea, as I'm talking without knowing any about the fiber codebase yet, so please correct me if this is nonsense 😅

@gaby
Copy link
Member Author

gaby commented Aug 31, 2024

@mdelapenya Yeah that approach makes sense, I will add it to the idea pool

@wangjq4214
Copy link
Member

On the applications I currently use Fiber in production, we create a whole set of application components using a single pointer of structs, and perform the Dependency Injection at the bootstrap of the application to provide this "Application State". I will give an example on how we perform it today:

func main() {
	app := fiber.New()
	service := UserService{}
	handler := UserHandler{
		service: service,
	}
	app.Get("/users/:id", handler.GetUser)
	app.Listen(":3000")
}

type User struct{}

type UserService struct {
}

func (*UserService) GetUser(id string) (*User, error) {
	return nil, nil
}

type UserHandler struct{
	service UserService
}

func (handler *UserHandler) GetUser(ctx fiber.Ctx) error {
	_, _ = handler.service.GetUser(ctx.Get(":id"))
	return nil
}

In any case, such solution (Fiber's Application State) could help us improve the solution we have today.

I think it is enough. An app-level state manager may not display the benefit.
But now I commit to a rust web server, axum provide a similar impl, but it enforces the handler is a stateless function

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Todo
Development

No branches or pull requests

5 participants