Skip to content

Commit 488a771

Browse files
authored
Merge pull request #7 from flant/fix_jq_call_loop_problem
fix: explicit JqCallLoop can lead to runtime C errors
2 parents 1afb898 + bfac540 commit 488a771

File tree

11 files changed

+306
-167
lines changed

11 files changed

+306
-167
lines changed

README.md

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,48 @@ CGO bindings for jq with cache for compiled programs
77
```
88
import (
99
"fmt"
10-
. "github.com/flant/libjq-go" // import Jq, JqMainThread and JqCallLoop
10+
. "github.com/flant/libjq-go" // import Jq() shortcut
1111
)
1212
1313
func main() {
14-
// Jq instance with direct calls of libjq methods. Note that it cannot be used in go routines.
15-
var jq = JqMainThread
16-
17-
// Run one program with one input.
18-
res, err := jq().Program(".foo").Run(`{"foo":"bar"}`)
19-
20-
// Use directory with jq modules.
21-
res, err := jq().WithLibPath("./jq_lib").
22-
Program(...).
23-
Run(...)
24-
25-
// Use jq state cache to speedup handling of multiple inputs.
26-
prg, err := jq().Program(...).Precompile()
27-
for _, data := range InputJsons {
28-
res, err = prg.Run(data)
29-
// do something with filter result ...
30-
}
31-
32-
// Use jq from go-routines.
33-
// Jq() helper returns instance that use LockOsThread trick to run libjq methods in main thread.
34-
done := make(chan struct{})
35-
36-
go func() {
37-
res, err := Jq().Program(".foo").Run(`{"foo":"bar"}`)
38-
done <- struct{}{}
39-
}()
40-
41-
// main is locked here.
42-
JqCallLoop(done)
14+
// 1. Run one program with one input.
15+
res, err = Jq().Program(".foo").Run(`{"foo":"bar"}`)
16+
17+
// 2. Use directory with jq modules.
18+
res, err = Jq().WithLibPath("./jq_lib").
19+
Program(`....`).
20+
Run(`...`)
21+
22+
// 3. Use program text as a key for a cache.
23+
for _, data := range inputJsons {
24+
res, err = Jq().Program(".foo").Cached().Run(data)
25+
// Do something with result ...
26+
}
27+
28+
// 4. Explicitly precompile jq expression.
29+
prg, err := Jq().Program(".foo").Precompile()
30+
for _, data := range inputJsons {
31+
res, err = prg.Run(data)
32+
// Do something with result ...
33+
}
34+
35+
// 5. It is safe to use Jq() from multiple go-routines.
36+
// Note however that programs are executed synchronously.
37+
go func() {
38+
res, err = Jq().Program(".foo").Run(`{"foo":"bar"}`)
39+
}()
40+
go func() {
41+
res, err = Jq().Program(".foo").Cached().Run(`{"foo":"bar"}`)
42+
}()
4343
}
4444
```
4545

46+
This code is available in [example.go](example/example.go) as a working example.
4647

4748
## Build
4849

50+
1. Local build
51+
4952
To build your program with this library, you should install some build dependencies and statically compile oniguruma and jq libraries:
5053

5154
```
@@ -64,6 +67,10 @@ Now you can build your application:
6467
CGO_ENABLED=1 CGO_CFLAGS="${LIBJQ_CFLAGS}" CGO_LDFLAGS="${LIBJQ_LDFLAGS}" go build <your arguments>
6568
```
6669

70+
2. Docker build
71+
72+
If you want to build your program with docker, you can build oniguruma and jq in artifact image and then copy them to go builder image. See example of this approach in [Dockerfile](https://github.com/flant/shell-operator/blob/master/Dockerfile) of a shell-operator — the real project that use this library.
73+
6774
## Inspired projects
6875

6976
There are other `jq` bindings in Go:

example/example.go

Lines changed: 117 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,143 @@ package main
22

33
import (
44
"fmt"
5+
"io/ioutil"
6+
"os"
7+
"sync"
58

69
. "github.com/flant/libjq-go"
710
)
811

12+
/*
13+
Run it locally if oniguruma and jq are compiled:
14+
15+
CGO_ENABLED=1 \
16+
CGO_CFLAGS="-Ipath-to-jq_lib/include" \
17+
CGO_LDFLAGS="-Lpath-to-oniguruma_lib/lib -Lpath-to-jq_lib/lib" \
18+
go run example.go
19+
20+
1. "bar"
21+
2. kebab-string-here "kebabStringHere"
22+
3. "bar-quux"
23+
3. "baz-baz"
24+
4. "Foo quux"
25+
4. "Foo baz"
26+
5. "bar"
27+
5. "bar"
28+
5. "bar"
29+
30+
*/
31+
932
func main() {
1033
var res string
1134
var err error
35+
var inputJsons []string
1236

13-
// Jq instance with direct calls of libjq methods — cannot be used is go routines.
14-
var jq = JqMainThread
15-
16-
// Run one program with one input.
17-
res, err = jq().Program(".foo").Run(`{"foo":"bar"}`)
37+
// 1. Run one program with one input.
38+
res, err = Jq().Program(".foo").Run(`{"foo":"bar"}`)
1839
if err != nil {
1940
panic(err)
2041
}
21-
fmt.Printf("filter result: %s\n", res)
42+
fmt.Printf("1. %s\n", res)
43+
// Should print
44+
// 1. "bar"
45+
46+
// 2. Use directory with jq modules.
47+
prepareJqLib()
48+
res, err = Jq().WithLibPath("./jq_lib").
49+
Program(`include "mylibrary"; .foo|mymethod`).
50+
Run(`{"foo":"kebab-string-here"}`)
51+
fmt.Printf("2. %s %s\n", "kebab-string-here", res)
52+
removeJqLib()
53+
// Should print
54+
// 2. kebab-string-here "kebabStringHere"
55+
56+
// 3. Use program text as a key for a cache.
57+
inputJsons = []string{
58+
`{ "foo":"bar-quux" }`,
59+
`{ "foo":"baz-baz" }`,
60+
// ...
61+
}
62+
for _, data := range inputJsons {
63+
res, err = Jq().Program(".foo").Cached().Run(data)
64+
if err != nil {
65+
panic(err)
66+
}
67+
// Now do something with filter result ...
68+
fmt.Printf("3. %s\n", res)
69+
}
70+
// Should print
71+
// 3. "bar-quux"
72+
// 3. "baz-baz"
2273

23-
// Use jq state cache to speedup handling of multiple inputs.
24-
InputJson := []string{
25-
`{..fields..}`,
26-
`{..fields..}`,
74+
// 4. Explicitly precompile jq expression.
75+
inputJsons = []string{
76+
`{ "bar":"Foo quux" }`,
77+
`{ "bar":"Foo baz" }`,
78+
// ...
2779
}
28-
jqp, err := jq().Program(".[]|.bar").Precompile()
80+
prg, err := Jq().Program(".bar").Precompile()
2981
if err != nil {
3082
panic(err)
3183
}
32-
for _, data := range InputJson {
33-
res, err = jqp.Run(data)
34-
// do something with filter result ...
84+
for _, data := range inputJsons {
85+
res, err = prg.Run(data)
86+
if err != nil {
87+
panic(err)
88+
}
89+
// Now do something with filter result ...
90+
fmt.Printf("4. %s\n", res)
3591
}
92+
// Should print
93+
// 4. "Foo quux"
94+
// 4. "Foo baz"
3695

37-
// Use directory with jq modules.
38-
res, err = jq().WithLibPath("./jq_lib").
39-
Program(`include "libname"; .foo|libmethod`).
40-
Run(`{"foo":"json here"}`)
41-
42-
// Use jq from go-routines.
43-
// Jq() returns instance that use LockOsThread trick to run libjq methods in main thread.
44-
done := make(chan struct{})
45-
96+
// 5. It is safe to use Jq() from multiple go-routines.
97+
// Note however that programs are executed synchronously.
98+
wg := sync.WaitGroup{}
99+
wg.Add(3)
46100
go func() {
47101
res, err = Jq().Program(".foo").Run(`{"foo":"bar"}`)
48-
done <- struct{}{}
102+
if err != nil {
103+
panic(err)
104+
}
105+
fmt.Printf("5. %s\n", res)
106+
wg.Done()
107+
}()
108+
go func() {
109+
res, err = Jq().Program(".foo").Cached().Run(`{"foo":"bar"}`)
110+
if err != nil {
111+
panic(err)
112+
}
113+
fmt.Printf("5. %s\n", res)
114+
wg.Done()
115+
}()
116+
go func() {
117+
res, err = Jq().Program(".foo").Cached().Run(`{"foo":"bar"}`)
118+
if err != nil {
119+
panic(err)
120+
}
121+
fmt.Printf("5. %s\n", res)
122+
wg.Done()
49123
}()
124+
wg.Wait()
125+
// Should print
126+
// 5. "bar"
127+
// 5. "bar"
128+
// 5. "bar"
129+
}
130+
131+
func prepareJqLib() {
132+
if err := os.MkdirAll("./jq_lib", 0755); err != nil {
133+
panic(err)
134+
}
135+
if err := ioutil.WriteFile("./jq_lib/mylibrary.jq", []byte(`def mymethod: gsub("-(?<a>[a-z])"; .a|ascii_upcase);`), 0644); err != nil {
136+
panic(err)
137+
}
138+
}
50139

51-
// main is locked here.
52-
JqCallLoop(done)
140+
func removeJqLib() {
141+
if err := os.RemoveAll("./jq_lib"); err != nil {
142+
panic(err)
143+
}
53144
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ module github.com/flant/libjq-go
22

33
go 1.12
44

5-
require github.com/stretchr/testify v1.4.0
5+
require github.com/onsi/gomega v1.5.0

go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
11
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
22
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
4+
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
5+
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
6+
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
7+
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
8+
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
39
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
410
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
511
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
612
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
713
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
14+
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
15+
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
16+
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
17+
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
18+
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
19+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
820
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
21+
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
22+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
23+
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
24+
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
925
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
1026
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

jq.go

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,11 @@
11
package libjq_go
22

3-
import "github.com/flant/libjq-go/pkg/jq"
3+
import (
4+
"github.com/flant/libjq-go/pkg/jq"
5+
)
46

5-
// Jq is a default jq invoker with a cache for programs and a jq calls proxy
7+
// Jq is handy shortcut to use a default jq invoker with enabled cache for programs
68
func Jq() *jq.Jq {
7-
return jq.NewJq().
8-
WithCache(jq.JqDefaultCache()).
9-
WithCallProxy(jq.JqCall)
10-
}
11-
12-
func JqMainThread() *jq.Jq {
139
return jq.NewJq().
1410
WithCache(jq.JqDefaultCache())
1511
}
16-
17-
// JqCallLoop should be called from main.main method to make Jq.Program.Run calls from go routines.
18-
//
19-
// Note: this method locks thread execution.
20-
func JqCallLoop(done chan struct{}) {
21-
jq.JqCallLoop(done)
22-
}

jq_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,19 @@
11
package libjq_go
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/gomega"
7+
)
8+
9+
func Test_OneProgram_OneInput(t *testing.T) {
10+
g := NewWithT(t)
11+
12+
res, err := Jq().Program(".foo").Run(`{"foo":"bar"}`)
13+
g.Expect(err).ShouldNot(HaveOccurred())
14+
g.Expect(res).To(Equal(`"bar"`))
15+
16+
res, err = Jq().Program(".foo").RunRaw(`{"foo":"bar"}`)
17+
g.Expect(err).ShouldNot(HaveOccurred())
18+
g.Expect(res).To(Equal(`bar`))
19+
}

pkg/jq/call_loop.go

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)