add Context.SendFileWithRate, ServeFileWithRate and ServeContentWithRate

as requested at: https://github.com/kataras/iris/issues/1493


Former-commit-id: 7783fde04b4247056e6309e7ec1df27f027dc655
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-05-02 17:46:17 +03:00
parent 1e1d8a4855
commit dbd6fcd2d7
8 changed files with 223 additions and 105 deletions

View File

@ -394,31 +394,33 @@ Other Improvements:
New Context Methods: New Context Methods:
- `context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2 - `Context.ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` methods to throttle the "download" speed of the client.
- `context.IsGRPC() bool` reports whether the request came from a gRPC client - `Context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2
- `context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too - `Context.IsGRPC() bool` reports whether the request came from a gRPC client
- `context.StopWithStatus(int)` stops the handlers chain and writes the status code - `Context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too
- `context.StopWithText(int, string)` stops the handlers chain, writes thre status code and a plain text message - `Context.StopWithStatus(int)` stops the handlers chain and writes the status code
- `context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message - `Context.StopWithText(int, string)` stops the handlers chain, writes thre status code and a plain text message
- `context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response - `Context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message
- `context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response - `Context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response
- `context.Protobuf(proto.Message)` sends protobuf to the client - `Context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response
- `context.MsgPack(interface{})` sends msgpack format data to the client - `Context.Protobuf(proto.Message)` sends protobuf to the client
- `context.ReadProtobuf(ptr)` binds request body to a proto message - `Context.MsgPack(interface{})` sends msgpack format data to the client
- `context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct - `Context.ReadProtobuf(ptr)` binds request body to a proto message
- `context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type - `Context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct
- `context.SetSameSite(http.SameSite)` to set cookie "SameSite" option (respectful by sessions too) - `Context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type
- `context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead - `Context.SetSameSite(http.SameSite)` to set cookie "SameSite" option (respectful by sessions too)
- `context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(context)` - `Context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead
- `context.Controller() reflect.Value` returns the current MVC Controller value. - `Context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(ctx)`
- `Context.Controller() reflect.Value` returns the current MVC Controller value.
Breaking Changes: Breaking Changes:
Change the MIME type of `Javascript .js` and `JSONP` as the HTML specification now recommends to `"text/javascript"` instead of the obselete `"application/javascript"`. This change was pushed to the `Go` language itself as well. See <https://go-review.googlesource.com/c/go/+/186927/>. - Change the MIME type of `Javascript .js` and `JSONP` as the HTML specification now recommends to `"text/javascript"` instead of the obselete `"application/javascript"`. This change was pushed to the `Go` language itself as well. See <https://go-review.googlesource.com/c/go/+/186927/>.
- Remove the last input argument of `enableGzipCompression` in `Context.ServeContent`, `ServeFile` methods. This was deprecated a few versions ago. A middleware (`app.Use(iris.Gzip)`) or a prior call to `Context.Gzip(true)` will enable gzip compression. Also these two methods and `Context.SendFile` one now support `Content-Range` and `Accept-Ranges` correctly out of the box (`net/http` had a bug, which is now fixed).
- `Context.ServeContent` no longer returns an error, see `ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` new methods too.
- `route.Trace() string` changed to `route.Trace(w io.Writer)`, to achieve the same result just pass a `bytes.Buffer` - `route.Trace() string` changed to `route.Trace(w io.Writer)`, to achieve the same result just pass a `bytes.Buffer`
- `var mvc.AutoBinding` removed as the default behavior now resolves such dependencies automatically (see [[FEATURE REQUEST] MVC serving gRPC-compatible controller](https://github.com/kataras/iris/issues/1449)) - `var mvc.AutoBinding` removed as the default behavior now resolves such dependencies automatically (see [[FEATURE REQUEST] MVC serving gRPC-compatible controller](https://github.com/kataras/iris/issues/1449)).
- `mvc#Application.SortByNumMethods()` removed as the default behavior now binds the "thinnest" empty `interface{}` automatically (see [MVC: service injecting fails](https://github.com/kataras/iris/issues/1343)) - `mvc#Application.SortByNumMethods()` removed as the default behavior now binds the "thinnest" empty `interface{}` automatically (see [MVC: service injecting fails](https://github.com/kataras/iris/issues/1343)).
- `mvc#BeforeActivation.Dependencies().Add` should be replaced with `mvc#BeforeActivation.Dependencies().Register` instead - `mvc#BeforeActivation.Dependencies().Add` should be replaced with `mvc#BeforeActivation.Dependencies().Register` instead
- **REMOVE** the `kataras/iris/v12/typescript` package in favor of the new [iris-cli](https://github.com/kataras/iris-cli). Also, the alm typescript online editor was removed as it is deprecated by its author, please consider using the [designtsx](https://designtsx.com/) instead. - **REMOVE** the `kataras/iris/v12/typescript` package in favor of the new [iris-cli](https://github.com/kataras/iris-cli). Also, the alm typescript online editor was removed as it is deprecated by its author, please consider using the [designtsx](https://designtsx.com/) instead.

View File

@ -237,7 +237,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
- [Basic](file-server/basic/main.go) - [Basic](file-server/basic/main.go)
- [Embedding Files Into App Executable File](file-server/embedding-files-into-app/main.go) - [Embedding Files Into App Executable File](file-server/embedding-files-into-app/main.go)
- [Embedding Gziped Files Into App Executable File](file-server/embedding-gziped-files-into-app/main.go) - [Embedding Gziped Files Into App Executable File](file-server/embedding-gziped-files-into-app/main.go)
- [Send/Force-Download Files](file-server/send-files/main.go) - [Send/Force-Download Files](file-server/send-files/main.go) **UPDATED**
- Single Page Applications - Single Page Applications
* [single Page Application](file-server/single-page-application/basic/main.go) * [single Page Application](file-server/single-page-application/basic/main.go)
* [embedded Single Page Application](file-server/single-page-application/embedded-single-page-application/main.go) * [embedded Single Page Application](file-server/single-page-application/embedded-single-page-application/main.go)
@ -246,7 +246,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
### How to Read from `context.Request() *http.Request` ### How to Read from `context.Request() *http.Request`
- [Read JSON](http_request/read-json/main.go) - [Read JSON](http_request/read-json/main.go)
* [Struct Validation](http_request/read-json-struct-validation/main.go) **UPDaTE** * [Struct Validation](http_request/read-json-struct-validation/main.go) **UPDATED**
- [Read XML](http_request/read-xml/main.go) - [Read XML](http_request/read-xml/main.go)
- [Read MsgPack](http_request/read-msgpack/main.go) **NEW** - [Read MsgPack](http_request/read-msgpack/main.go) **NEW**
- [Read YAML](http_request/read-yaml/main.go) - [Read YAML](http_request/read-yaml/main.go)

View File

@ -6,11 +6,26 @@ import (
func main() { func main() {
app := iris.New() app := iris.New()
app.Logger().SetLevel("debug")
app.Get("/", func(ctx iris.Context) { app.Get("/", download)
file := "./files/first.zip" app.Get("/download", downloadWithRateLimit)
ctx.SendFile(file, "c.zip")
})
app.Listen(":8080") app.Listen(":8080")
} }
func download(ctx iris.Context) {
src := "./files/first.zip"
ctx.SendFile(src, "client.zip")
}
func downloadWithRateLimit(ctx iris.Context) {
// REPLACE THAT WITH A BIG LOCAL FILE OF YOUR OWN.
src := "./files/first.zip"
dest := "" /* optionally, keep it empty to resolve the filename based on the "src" */
// Limit download speed to ~50Kb/s with a burst of 100KB.
limit := 50.0 * iris.KB
burst := 100 * iris.KB
ctx.SendFileWithRate(src, dest, limit, burst)
}

View File

@ -4,6 +4,9 @@ import "github.com/kataras/iris/v12"
func main() { func main() {
app := iris.New() app := iris.New()
// app.Use(iris.Gzip)
// func(ctx iris.Context) { ctx.Gzip(true/false)}
// OR:
app.Get("/", func(ctx iris.Context) { app.Get("/", func(ctx iris.Context) {
ctx.WriteGzip([]byte("Hello World!")) ctx.WriteGzip([]byte("Hello World!"))
ctx.Header("X-Custom", ctx.Header("X-Custom",

View File

@ -2,6 +2,7 @@ package context
import ( import (
"bytes" "bytes"
stdContext "context"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"errors" "errors"
@ -35,6 +36,7 @@ import (
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
"github.com/vmihailenco/msgpack/v5" "github.com/vmihailenco/msgpack/v5"
"golang.org/x/time/rate"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -916,31 +918,57 @@ type Context interface {
// | Serve files | // | Serve files |
// +------------------------------------------------------------+ // +------------------------------------------------------------+
// ServeContent serves content, headers are autoset // ServeContent replies to the request using the content in the
// receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string) // provided ReadSeeker. The main benefit of ServeContent over io.Copy
// is that it handles Range requests properly, sets the MIME type, and
// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,
// and If-Range requests.
// //
// If the response's Content-Type header is not set, ServeContent
// first tries to deduce the type from name's file extension.
// //
// You can define your own "Content-Type" with `context#ContentType`, before this function call. // The name is otherwise unused; in particular it can be empty and is
// never sent in the response.
// //
// This function doesn't support resuming (by range), // If modtime is not the zero time or Unix epoch, ServeContent
// use ctx.SendFile or router's `HandleDir` instead. // includes it in a Last-Modified header in the response. If the
ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error // request includes an If-Modified-Since header, ServeContent uses
// ServeFile serves a file (to send a file, a zip for example to the client you should use the `SendFile` instead) // modtime to decide whether the content needs to be sent at all.
// receives two parameters
// filename/path (string)
// gzipCompression (bool)
// //
// You can define your own "Content-Type" with `context#ContentType`, before this function call. // The content's Seek method must work: ServeContent uses
// a seek to the end of the content to determine its size.
// //
// This function doesn't support resuming (by range), // If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
// use ctx.SendFile or router's `HandleDir` instead. // ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
// //
// Use it when you want to serve dynamic files to the client. // Note that *os.File implements the io.ReadSeeker interface.
ServeFile(filename string, gzipCompression bool) error // Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
// SendFile sends file for force-download to the client ServeContent(content io.ReadSeeker, filename string, modtime time.Time)
// ServeContentWithRate same as `ServeContent` but it can throttle the speed of reading
// and though writing the "content" to the client.
ServeContentWithRate(content io.ReadSeeker, filename string, modtime time.Time, limit float64, burst int)
// ServeFile replies to the request with the contents of the named
// file or directory.
// //
// Use this instead of ServeFile to 'force-download' bigger files to the client. // If the provided file or directory name is a relative path, it is
// interpreted relative to the current directory and may ascend to
// parent directories. If the provided name is constructed from user
// input, it should be sanitized before calling `ServeFile`.
//
// Use it when you want to serve assets like css and javascript files.
// If client should confirm and save the file use the `SendFile` instead.
// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
ServeFile(filename string) error
// ServeFileWithRate same as `ServeFile` but it can throttle the speed of reading
// and though writing the file to the client.
ServeFileWithRate(filename string, limit float64, burst int) error
// SendFile sends a file as an attachment, that is downloaded and saved locally from client.
// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
// Use `ServeFile` if a file should be served as a page asset instead.
SendFile(filename string, destinationName string) error SendFile(filename string, destinationName string) error
// SendFileWithRate same as `SendFile` but it can throttle the speed of reading
// and though writing the file to the client.
SendFileWithRate(src, destName string, limit float64, burst int) error
// +------------------------------------------------------------+ // +------------------------------------------------------------+
// | Cookies | // | Cookies |
@ -4544,65 +4572,135 @@ func (n *NegotiationAcceptBuilder) EncodingGzip() *NegotiationAcceptBuilder {
// | Serve files | // | Serve files |
// +------------------------------------------------------------+ // +------------------------------------------------------------+
// ServeContent serves content, headers are autoset // ServeContent replies to the request using the content in the
// receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string) // provided ReadSeeker. The main benefit of ServeContent over io.Copy
// is that it handles Range requests properly, sets the MIME type, and
// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,
// and If-Range requests.
// //
// You can define your own "Content-Type" header also, after this function call // If the response's Content-Type header is not set, ServeContent
// Doesn't implements resuming (by range), use ctx.SendFile instead // first tries to deduce the type from name's file extension.
func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error { //
if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil { // The name is otherwise unused; in particular it can be empty and is
ctx.WriteNotModified() // never sent in the response.
return nil //
// If modtime is not the zero time or Unix epoch, ServeContent
// includes it in a Last-Modified header in the response. If the
// request includes an If-Modified-Since header, ServeContent uses
// modtime to decide whether the content needs to be sent at all.
//
// The content's Seek method must work: ServeContent uses
// a seek to the end of the content to determine its size.
//
// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
//
// Note that *os.File implements the io.ReadSeeker interface.
// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time) {
ctx.ServeContentWithRate(content, filename, modtime, 0, 0)
}
// rateReadSeeker is a io.ReadSeeker that is rate limited by
// the given token bucket. Each token in the bucket
// represents one byte. See "golang.org/x/time/rate" package.
type rateReadSeeker struct {
io.ReadSeeker
ctx stdContext.Context
limiter *rate.Limiter
}
func (rs *rateReadSeeker) Read(buf []byte) (int, error) {
n, err := rs.ReadSeeker.Read(buf)
if n <= 0 {
return n, err
}
rs.limiter.WaitN(rs.ctx, n)
return n, err
}
// ServeContentWithRate same as `ServeContent` but it can throttle the speed of reading
// and though writing the "content" to the client.
func (ctx *context) ServeContentWithRate(content io.ReadSeeker, filename string, modtime time.Time, limit float64, burst int) {
if limit > 0 {
content = &rateReadSeeker{
ReadSeeker: content,
ctx: ctx.request.Context(),
limiter: rate.NewLimiter(rate.Limit(limit), burst),
}
} }
if ctx.GetContentType() == "" { if ctx.GetContentType() == "" {
ctx.ContentType(filename) ctx.ContentType(filename)
} }
ctx.SetLastModified(modtime) http.ServeContent(ctx.writer, ctx.request, filename, modtime, content)
var out io.Writer
if gzipCompression && ctx.ClientSupportsGzip() {
AddGzipHeaders(ctx.writer)
gzipWriter := acquireGzipWriter(ctx.writer)
defer releaseGzipWriter(gzipWriter)
out = gzipWriter
} else {
out = ctx.writer
}
_, err := io.Copy(out, content)
return err ///TODO: add an int64 as return value for the content length written like other writers or let it as it's in order to keep the stable api?
} }
// ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename) // ServeFile replies to the request with the contents of the named
// receives two parameters // file or directory.
// filename/path (string)
// gzipCompression (bool)
// //
// You can define your own "Content-Type" header also, after this function call // If the provided file or directory name is a relative path, it is
// This function doesn't implement resuming (by range), use ctx.SendFile instead // interpreted relative to the current directory and may ascend to
// parent directories. If the provided name is constructed from user
// input, it should be sanitized before calling `ServeFile`.
// //
// Use it when you want to serve css/js/... files to the client, for bigger files and 'force-download' use the SendFile. // Use it when you want to serve assets like css and javascript files.
func (ctx *context) ServeFile(filename string, gzipCompression bool) error { // If client should confirm and save the file use the `SendFile` instead.
// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
func (ctx *context) ServeFile(filename string) error {
return ctx.ServeFileWithRate(filename, 0, 0)
}
// ServeFileWithRate same as `ServeFile` but it can throttle the speed of reading
// and though writing the file to the client.
func (ctx *context) ServeFileWithRate(filename string, limit float64, burst int) error {
f, err := os.Open(filename) f, err := os.Open(filename)
if err != nil { if err != nil {
return fmt.Errorf("%d", http.StatusNotFound) ctx.StatusCode(http.StatusNotFound)
return err
} }
defer f.Close() defer f.Close()
fi, _ := f.Stat()
if fi.IsDir() { st, err := f.Stat()
return ctx.ServeFile(path.Join(filename, "index.html"), gzipCompression) if err != nil {
code := http.StatusInternalServerError
if os.IsNotExist(err) {
code = http.StatusNotFound
} }
return ctx.ServeContent(f, fi.Name(), fi.ModTime(), gzipCompression) if os.IsPermission(err) {
code = http.StatusForbidden
}
ctx.StatusCode(code)
return err
}
if st.IsDir() {
return ctx.ServeFile(path.Join(filename, "index.html"))
}
ctx.ServeContentWithRate(f, st.Name(), st.ModTime(), limit, burst)
return nil
} }
// SendFile sends file for force-download to the client // SendFile sends a file as an attachment, that is downloaded and saved locally from client.
// // Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`.
// Use this instead of ServeFile to 'force-download' bigger files to the client. // Use `ServeFile` if a file should be served as a page asset instead.
func (ctx *context) SendFile(filename string, destinationName string) error { func (ctx *context) SendFile(src string, destName string) error {
ctx.writer.Header().Set(ContentDispositionHeaderKey, "attachment;filename="+destinationName) return ctx.SendFileWithRate(src, destName, 0, 0)
return ctx.ServeFile(filename, false) }
// SendFileWithRate same as `SendFile` but it can throttle the speed of reading
// and though writing the file to the client.
func (ctx *context) SendFileWithRate(src, destName string, limit float64, burst int) error {
if destName == "" {
destName = filepath.Base(src)
}
ctx.writer.Header().Set(ContentDispositionHeaderKey, "attachment;filename="+destName)
return ctx.ServeFileWithRate(src, limit, burst)
} }
// +------------------------------------------------------------+ // +------------------------------------------------------------+

11
iris.go
View File

@ -591,6 +591,17 @@ const (
ReferrerGoogleAdwords = context.ReferrerGoogleAdwords ReferrerGoogleAdwords = context.ReferrerGoogleAdwords
) )
// Byte unit helpers.
const (
B = 1 << (10 * iota)
KB
MB
GB
TB
PB
EB
)
// ConfigureHost accepts one or more `host#Configuration`, these configurators functions // ConfigureHost accepts one or more `host#Configuration`, these configurators functions
// can access the host created by `app.Run`, // can access the host created by `app.Run`,
// they're being executed when application is ready to being served to the public. // they're being executed when application is ready to being served to the public.

View File

@ -50,10 +50,11 @@ type (
Limiter struct { Limiter struct {
clientDataFunc func(ctx context.Context) interface{} // fill the Client's Data field. clientDataFunc func(ctx context.Context) interface{} // fill the Client's Data field.
exceedHandler context.Handler // when too many requests. exceedHandler context.Handler // when too many requests.
limit rate.Limit
burstSize int
clients map[string]*Client clients map[string]*Client
mu sync.RWMutex // mutex for clients. mu sync.RWMutex // mutex for clients.
pool *sync.Pool // object pool for clients.
} }
Client struct { Client struct {
@ -68,14 +69,10 @@ type (
const Inf = math.MaxFloat64 const Inf = math.MaxFloat64
func Limit(limit float64, burst int, options ...Option) context.Handler { func Limit(limit float64, burst int, options ...Option) context.Handler {
rateLimit := rate.Limit(limit)
l := &Limiter{ l := &Limiter{
clients: make(map[string]*Client), clients: make(map[string]*Client),
pool: &sync.Pool{New: func() interface{} { limit: rate.Limit(limit),
return &Client{limiter: rate.NewLimiter(rateLimit, burst)} burstSize: burst,
}},
exceedHandler: func(ctx context.Context) { exceedHandler: func(ctx context.Context) {
ctx.StopWithStatus(429) // Too Many Requests. ctx.StopWithStatus(429) // Too Many Requests.
}, },
@ -88,21 +85,10 @@ func Limit(limit float64, burst int, options ...Option) context.Handler {
return l.serveHTTP return l.serveHTTP
} }
func (l *Limiter) acquire() *Client {
return l.pool.Get().(*Client)
}
func (l *Limiter) release(client *Client) {
client.IP = ""
client.Data = nil
l.pool.Put(client)
}
func (l *Limiter) Purge(condition func(*Client) bool) { func (l *Limiter) Purge(condition func(*Client) bool) {
l.mu.Lock() l.mu.Lock()
for ip, client := range l.clients { for ip, client := range l.clients {
if condition(client) { if condition(client) {
l.release(client)
delete(l.clients, ip) delete(l.clients, ip)
} }
} }
@ -116,12 +102,15 @@ func (l *Limiter) serveHTTP(ctx context.Context) {
l.mu.RUnlock() l.mu.RUnlock()
if !ok { if !ok {
client = l.acquire() client = &Client{
client.IP = ip limiter: rate.NewLimiter(l.limit, l.burstSize),
IP: ip,
}
if l.clientDataFunc != nil { if l.clientDataFunc != nil {
client.Data = l.clientDataFunc(ctx) client.Data = l.clientDataFunc(ctx)
} }
// if l.store(ctx, client) { // if l.store(ctx, client) {
// ^ no, let's keep it simple. // ^ no, let's keep it simple.
l.mu.Lock() l.mu.Lock()