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

feat: Add Godror compatible driver #185

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions godrorstore/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# godrorstore

An Oracle Database based session store for [SCS](https://github.com/alexedwards/scs) using the [godror](https://github.com/godror/godror) driver.

## Setup

You should have a working Oracle database containing a `sessions` table with the definition:

```sql
CREATE TABLE sessions (
token varchar(250) PRIMARY KEY,
data BLOB NOT NULL,
expiry TIMESTAMP NOT NULL
);

CREATE INDEX sessions_expiry_idx ON sessions (expiry);
```

The database user for your application must have `SELECT`, `INSERT`, `UPDATE` and `DELETE` permissions on this table.

## Example

```go
package main

import (
"database/sql"
"io"
"log"
"net/http"

"github.com/alexedwards/scs/v2"
"github.com/alexedwards/scs/godrorstore"

_ "github.com/godror/godror"
)

var sessionManager *scs.SessionManager

func main() {
// Configure connection string.
username := "username"
password := "password"
host := "example.com"
port := "12345" // Normally 1521
service := "service.world"
connStr := fmt.Sprintf(`user="%s" password="%s" connectString="%s:%s/%s" timezone="+0000"`, username, password, host, port, service)

// Establish connection to Oracle Database.
db, err := sql.Open("godror", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()

// Initialize a new session manager and configure it to use godrorstore as the session store.
sessionManager = scs.New()
sessionManager.Store = godrorstore.New(db)

mux := http.NewServeMux()
mux.HandleFunc("/put", putHandler)
mux.HandleFunc("/get", getHandler)

http.ListenAndServe(":4000", sessionManager.LoadAndSave(mux))
}

func putHandler(w http.ResponseWriter, r *http.Request) {
sessionManager.Put(r.Context(), "message", "Hello from a session!")
}

func getHandler(w http.ResponseWriter, r *http.Request) {
msg := sessionManager.GetString(r.Context(), "message")
io.WriteString(w, msg)
}
```

## Expired Session Cleanup

This package provides a background 'cleanup' goroutine to delete expired session data. This stops the database table from holding on to invalid sessions indefinitely and growing unnecessarily large. By default the cleanup runs every 5 minutes. You can change this by using the `NewWithCleanupInterval()` function to initialize your session store. For example:

```go
// Run a cleanup every 30 minutes.
godrorstore.NewWithCleanupInterval(db, 30*time.Minute)

// Disable the cleanup goroutine by setting the cleanup interval to zero.
godrorstore.NewWithCleanupInterval(db, 0)
```

### Terminating the Cleanup Goroutine

It's rare that the cleanup goroutine needs to be terminated --- it is generally intended to be long-lived and run for the lifetime of your application.

However, there may be occasions when your use of a session store instance is transient. A common example would be using it in a short-lived test function. In this scenario, the cleanup goroutine (which will run forever) will prevent the session store instance from being garbage collected even after the test function has finished. You can prevent this by either disabling the cleanup goroutine altogether (as described above) or by stopping it using the `StopCleanup()` method. For example:

```go
func TestExample(t *testing.T) {
db, err := sql.Open("godror", connStr)
if err != nil {
t.Fatal(err)
}
defer db.Close()

store := godrorstore.New(db)
defer store.StopCleanup()

sessionManager = scs.New()
sessionManager.Store = store

// Run test...
}
```
5 changes: 5 additions & 0 deletions godrorstore/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/chayes/scs/godrorstore

go 1.20

require github.com/godror/godror v0.39.0
128 changes: 128 additions & 0 deletions godrorstore/godrorstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package godrorstore

import (
"database/sql"
"fmt"
"log"
"time"
)

type GodrorStore struct {
db *sql.DB
stopCleanup chan bool
}

func New(db *sql.DB) *GodrorStore {
return NewWithCleanupInterval(db, 5*time.Minute)
}

func NewWithCleanupInterval(db *sql.DB, cleanupInterval time.Duration) *GodrorStore {
g := &GodrorStore{db: db}
if cleanupInterval > 0 {
go g.StartCleanup(cleanupInterval)
}
return g
}

func (g *GodrorStore) Find(token string) (b []byte, exists bool, err error) {
stmt := fmt.Sprintf("SELECT data FROM sessions WHERE token = '%x' AND current_timestamp < expiry", token)
row := g.db.QueryRow(stmt)
err = row.Scan(&b)
if err == sql.ErrNoRows {
return nil, false, nil
} else if err != nil {
return nil, false, err
}
return b, true, nil
}

func (g *GodrorStore) Commit(token string, b []byte, expiry time.Time) error {
stmt := fmt.Sprintf("SELECT data FROM sessions WHERE token = '%x'", token)
row := g.db.QueryRow(stmt)
err := row.Err()
if row.Scan() == sql.ErrNoRows {
stmt = `INSERT INTO sessions (token, data, expiry) VALUES ('%x', '%x', to_timestamp('` + string(expiry.Format("2006-01-02 15:04:05.00")) + `', 'YYYY-MM-DD HH24:MI:SS.FF'))`
stmt = fmt.Sprintf(stmt, token, b)
_, err := g.db.Exec(stmt)
if err != nil {
return err
}
return nil
} else if err != nil {
return err
}

stmt = `UPDATE sessions SET data = '%x', expiry = to_timestamp('` + string(expiry.Format("2006-01-02 15:04:05.00")) + `', 'YYYY-MM-DD HH24:MI:SS.FF') WHERE token = '%x'`
stmt = fmt.Sprintf(stmt, b, token)
_, err = g.db.Exec(stmt)
if err != nil {
return err
}

return nil
}

func (g *GodrorStore) Delete(token string) error {
stmt := fmt.Sprintf("DELETE FROM session WHERE token = '%x'", token)
_, err := g.db.Exec(stmt)
return err
}

func (g *GodrorStore) All() (map[string][]byte, error) {
rows, err := g.db.Query("SELECT token, data FROM sessions WHERE current_timestamp < expiry")
if err != nil {
return nil, err
}
defer rows.Close()

sessions := make(map[string][]byte)

for rows.Next() {
var (
token string
data []byte
)

err = rows.Scan(&token, &data)
if err != nil {
return nil, err
}

sessions[token] = data
}

err = rows.Err()
if err != nil {
return nil, err
}

return sessions, nil
}

func (g *GodrorStore) StartCleanup(interval time.Duration) {
g.stopCleanup = make(chan bool)
ticker := time.NewTicker(interval)
for {
select {
case <-ticker.C:
err := g.deleteExpired()
if err != nil {
log.Println(err)
}
case <-g.stopCleanup:
ticker.Stop()
return
}
}
}

func (g *GodrorStore) StopCleanup() {
if g.stopCleanup != nil {
g.stopCleanup <- true
}
}

func (g *GodrorStore) deleteExpired() error {
_, err := g.db.Exec("DELETE FROM sessions WHERE expiry < current_timestamp")
return err
}
Loading