Skip to content

Commit

Permalink
Add a noexprlang build tag
Browse files Browse the repository at this point in the history
This module uses `github.com/expr-lang/expr` which, because of the
extensive use of the `reflect` package, disables go's compiler dead code
elimination which can lead to bigger binaries.

This commit adds a `noexprlang` build tag that allows jsm.go users that
do not use expression matching to entirely disable the use of the expr
module so that they can benefit from go's dead code elimination if they
are eligible to outside jsm.go.

References:
- https://golab.io/talks/getting-the-most-out-of-dead-code-elimination
- https://github.com/aarzilli/whydeadcode
- spf13/cobra#1956

Signed-off-by: Sylvain Rabot <sylvain@abstraction.fr>
  • Loading branch information
sylr committed Feb 13, 2025
1 parent 12d0253 commit a85f66d
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 118 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,10 @@ This can be used by the `Manager` to validate all API access.
```go
mgr, _ := jsm.New(nc, jsm.WithAPIValidation(new(SchemaValidator)))
```

## Build tag

This library provides a `noexprlang` build tag that disables expression matching
for Streams and Consumers queries. The purpose of this build tag is to disable
the use of the `github.com/expr-lang/expr` module that disables go compiler's dead
code elimination because it uses some types and functions of the `reflect` package.
57 changes: 0 additions & 57 deletions consumer_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ import (
"strconv"
"time"

"github.com/expr-lang/expr"
"github.com/nats-io/jsm.go/api"
"gopkg.in/yaml.v3"
)

type consumerMatcher func([]*Consumer) ([]*Consumer, error)
Expand Down Expand Up @@ -59,14 +57,6 @@ func ConsumerQueryApiLevelMin(level int) ConsumerQueryOpt {
}
}

// ConsumerQueryExpression filters the consumers using the expr expression language
func ConsumerQueryExpression(e string) ConsumerQueryOpt {
return func(q *consumerQuery) error {
q.expression = e
return nil
}
}

// ConsumerQueryLeaderServer finds clustered consumers where a certain node is the leader
func ConsumerQueryLeaderServer(server string) ConsumerQueryOpt {
return func(q *consumerQuery) error {
Expand Down Expand Up @@ -355,50 +345,3 @@ func (q *consumerQuery) matchApiLevel(consumers []*Consumer) ([]*Consumer, error
return (!q.invert && requiredLevel >= q.apiLevel) || (q.invert && requiredLevel < q.apiLevel)
})
}

func (q *consumerQuery) matchExpression(consumers []*Consumer) ([]*Consumer, error) {
if q.expression == "" {
return consumers, nil
}

var matched []*Consumer

for _, consumer := range consumers {
cfg := map[string]any{}
state := map[string]any{}

cfgBytes, _ := yaml.Marshal(consumer.Configuration())
yaml.Unmarshal(cfgBytes, &cfg)
nfo, _ := consumer.LatestState()
stateBytes, _ := yaml.Marshal(nfo)
yaml.Unmarshal(stateBytes, &state)

env := map[string]any{
"config": cfg,
"state": state,
"info": state,
"Info": nfo,
}

program, err := expr.Compile(q.expression, expr.Env(env), expr.AsBool())
if err != nil {
return nil, err
}

out, err := expr.Run(program, env)
if err != nil {
return nil, err
}

should, ok := out.(bool)
if !ok {
return nil, fmt.Errorf("expression did not return a boolean")
}

if should {
matched = append(matched, consumer)
}
}

return matched, nil
}
4 changes: 4 additions & 0 deletions jsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import (
"github.com/nats-io/jsm.go/api"
)

// ErrNoExprLangBuild warns that expression matching is disabled when compiling
// a go binary with the `noexprlang` build tag.
var ErrNoExprLangBuild = fmt.Errorf("binary has been built with `noexprlang` build tag and thus does not support expression matching")

// standard api responses with error embedded
type jetStreamResponseError interface {
ToError() error
Expand Down
127 changes: 127 additions & 0 deletions match.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//go:build !noexprlang

package jsm

import (
"fmt"

"github.com/expr-lang/expr"
"gopkg.in/yaml.v3"
)

// StreamQueryExpression filters the stream using the expr expression language
// Using this option with a binary built with the `noexprlang` build tag will
// always return [ErrNoExprLangBuild].
func StreamQueryExpression(e string) StreamQueryOpt {
return func(q *streamQuery) error {
q.expression = e
return nil
}
}

func (q *streamQuery) matchExpression(streams []*Stream) ([]*Stream, error) {
if q.expression == "" {
return streams, nil
}

var matched []*Stream

for _, stream := range streams {
cfg := map[string]any{}
state := map[string]any{}
info := map[string]any{}

cfgBytes, _ := yaml.Marshal(stream.Configuration())
yaml.Unmarshal(cfgBytes, &cfg)
nfo, _ := stream.LatestInformation()
nfoBytes, _ := yaml.Marshal(nfo)
yaml.Unmarshal(nfoBytes, &info)
stateBytes, _ := yaml.Marshal(nfo.State)
yaml.Unmarshal(stateBytes, &state)

env := map[string]any{
"config": cfg,
"state": state,
"info": info,
"Info": nfo,
}

program, err := expr.Compile(q.expression, expr.Env(env), expr.AsBool())
if err != nil {
return nil, err
}

out, err := expr.Run(program, env)
if err != nil {
return nil, err
}

should, ok := out.(bool)
if !ok {
return nil, fmt.Errorf("expression did not return a boolean")
}

if should {
matched = append(matched, stream)
}
}

return matched, nil
}

// ConsumerQueryExpression filters the consumers using the expr expression language
// Using this option with a binary built with the `noexprlang` build tag will
// always return [ErrNoExprLangBuild].
func ConsumerQueryExpression(e string) ConsumerQueryOpt {
return func(q *consumerQuery) error {
q.expression = e
return nil
}
}

func (q *consumerQuery) matchExpression(consumers []*Consumer) ([]*Consumer, error) {
if q.expression == "" {
return consumers, nil
}

var matched []*Consumer

for _, consumer := range consumers {
cfg := map[string]any{}
state := map[string]any{}

cfgBytes, _ := yaml.Marshal(consumer.Configuration())
yaml.Unmarshal(cfgBytes, &cfg)
nfo, _ := consumer.LatestState()
stateBytes, _ := yaml.Marshal(nfo)
yaml.Unmarshal(stateBytes, &state)

env := map[string]any{
"config": cfg,
"state": state,
"info": state,
"Info": nfo,
}

program, err := expr.Compile(q.expression, expr.Env(env), expr.AsBool())
if err != nil {
return nil, err
}

out, err := expr.Run(program, env)
if err != nil {
return nil, err
}

should, ok := out.(bool)
if !ok {
return nil, fmt.Errorf("expression did not return a boolean")
}

if should {
matched = append(matched, consumer)
}
}

return matched, nil
}
37 changes: 37 additions & 0 deletions match_noexpr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//go:build noexprlang

package jsm

// StreamQueryExpression filters the stream using the expr expression language
// Using this option with a binary built with the `noexprlang` build tag will
// always return [ErrNoExprLangBuild].
func StreamQueryExpression(e string) StreamQueryOpt {
return func(q *streamQuery) error {
q.expression = e
return ErrNoExprLangBuild
}
}

func (q *streamQuery) matchExpression(streams []*Stream) ([]*Stream, error) {
if q.expression == "" {
return streams, nil
}
return nil, ErrNoExprLangBuild
}

// ConsumerQueryExpression filters the consumers using the expr expression language
// Using this option with a binary built with the `noexprlang` build tag will
// always return [ErrNoExprLangBuild].
func ConsumerQueryExpression(e string) ConsumerQueryOpt {
return func(q *consumerQuery) error {
q.expression = e
return ErrNoExprLangBuild
}
}

func (q *consumerQuery) matchExpression(consumers []*Consumer) ([]*Consumer, error) {
if q.expression == "" {
return consumers, nil
}
return nil, ErrNoExprLangBuild
}
61 changes: 0 additions & 61 deletions stream_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,12 @@
package jsm

import (
"fmt"
"regexp"
"strconv"
"strings"
"time"

"github.com/expr-lang/expr"
"github.com/nats-io/jsm.go/api"
"gopkg.in/yaml.v3"
)

type streamMatcher func([]*Stream) ([]*Stream, error)
Expand Down Expand Up @@ -57,14 +54,6 @@ func StreamQueryApiLevelMin(level int) StreamQueryOpt {
}
}

// StreamQueryExpression filters the stream using the expr expression language
func StreamQueryExpression(e string) StreamQueryOpt {
return func(q *streamQuery) error {
q.expression = e
return nil
}
}

func StreamQueryIsSourced() StreamQueryOpt {
return func(q *streamQuery) error {
q.sourced = true
Expand Down Expand Up @@ -231,56 +220,6 @@ func (q *streamQuery) Filter(streams []*Stream) ([]*Stream, error) {
return matched, nil
}

func (q *streamQuery) matchExpression(streams []*Stream) ([]*Stream, error) {
if q.expression == "" {
return streams, nil
}

var matched []*Stream

for _, stream := range streams {
cfg := map[string]any{}
state := map[string]any{}
info := map[string]any{}

cfgBytes, _ := yaml.Marshal(stream.Configuration())
yaml.Unmarshal(cfgBytes, &cfg)
nfo, _ := stream.LatestInformation()
nfoBytes, _ := yaml.Marshal(nfo)
yaml.Unmarshal(nfoBytes, &info)
stateBytes, _ := yaml.Marshal(nfo.State)
yaml.Unmarshal(stateBytes, &state)

env := map[string]any{
"config": cfg,
"state": state,
"info": info,
"Info": nfo,
}

program, err := expr.Compile(q.expression, expr.Env(env), expr.AsBool())
if err != nil {
return nil, err
}

out, err := expr.Run(program, env)
if err != nil {
return nil, err
}

should, ok := out.(bool)
if !ok {
return nil, fmt.Errorf("expression did not return a boolean")
}

if should {
matched = append(matched, stream)
}
}

return matched, nil
}

func (q *streamQuery) matchLeaderServer(streams []*Stream) ([]*Stream, error) {
if q.leader == "" {
return streams, nil
Expand Down

0 comments on commit a85f66d

Please sign in to comment.