Skip to content

Commit 094b02e

Browse files
committed
Reimplemented JSMapFunction using js API
and removed JSRunner, JSServer since they are now unused.
1 parent c770934 commit 094b02e

File tree

5 files changed

+142
-466
lines changed

5 files changed

+142
-466
lines changed

js_map_fn.go

Lines changed: 27 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,51 @@
1-
// Copyright 2012-Present Couchbase, Inc.
2-
//
3-
// Use of this software is governed by the Business Source License included
4-
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
5-
// in that file, in accordance with the Business Source License, use of this
6-
// software will be governed by the Apache License, Version 2.0, included in
7-
// the file licenses/APL2.txt.
8-
91
package sgbucket
102

113
import (
4+
"context"
5+
_ "embed"
6+
"encoding/json"
127
"fmt"
138
"time"
149

15-
"github.com/robertkrimen/otto"
10+
"github.com/couchbase/sg-bucket/js"
1611
)
1712

18-
const kTaskCacheSize = 4
19-
20-
// A compiled JavaScript 'map' function, API-compatible with Couchbase Server 2.0.
21-
// Based on JSRunner, so this is not thread-safe; use its wrapper JSMapFunction for that.
22-
type jsMapTask struct {
23-
JSRunner
24-
output []*ViewRow
25-
}
26-
27-
// Compiles a JavaScript map function to a jsMapTask object.
28-
func newJsMapTask(funcSource string, timeout time.Duration) (JSServerTask, error) {
29-
mapper := &jsMapTask{}
30-
err := mapper.Init(funcSource, timeout)
31-
if err != nil {
32-
return nil, err
33-
}
34-
35-
// Implementation of the 'emit()' callback:
36-
mapper.DefineNativeFunction("emit", func(call otto.FunctionCall) otto.Value {
37-
key, err1 := call.ArgumentList[0].Export()
38-
value, err2 := call.ArgumentList[1].Export()
39-
if err1 != nil || err2 != nil {
40-
panic(fmt.Sprintf("Unsupported key or value types: emit(%#v,%#v): %v %v", key, value, err1, err2))
41-
}
42-
mapper.output = append(mapper.output, &ViewRow{Key: key, Value: value})
43-
return otto.UndefinedValue()
44-
})
45-
46-
mapper.Before = func() {
47-
mapper.output = []*ViewRow{}
48-
}
49-
mapper.After = func(result otto.Value, err error) (interface{}, error) {
50-
output := mapper.output
51-
mapper.output = nil
52-
return output, err
53-
}
54-
return mapper, nil
55-
}
13+
//go:embed js_map_fn.js
14+
var kMapFunctionJSWrapper string
5615

5716
//////// JSMapFunction
5817

5918
// A thread-safe wrapper around a jsMapTask, i.e. a Couchbase-Server-compatible JavaScript
6019
// 'map' function.
6120
type JSMapFunction struct {
62-
*JSServer
21+
service *js.Service
22+
timeout time.Duration
6323
}
6424

65-
func NewJSMapFunction(fnSource string, timeout time.Duration) *JSMapFunction {
25+
func NewJSMapFunction(jsHost js.ServiceHost, fnSource string, timeout time.Duration) *JSMapFunction {
26+
fnSource = fmt.Sprintf(kMapFunctionJSWrapper, fnSource)
6627
return &JSMapFunction{
67-
JSServer: NewJSServer(fnSource, timeout, kTaskCacheSize,
68-
func(fnSource string, timeout time.Duration) (JSServerTask, error) {
69-
return newJsMapTask(fnSource, timeout)
70-
}),
28+
service: js.NewService(jsHost, "Map", fnSource),
29+
timeout: timeout,
7130
}
7231
}
7332

7433
// Calls a jsMapTask.
75-
func (mapper *JSMapFunction) CallFunction(doc string, docid string, vbNo uint32, vbSeq uint64) ([]*ViewRow, error) {
76-
result1, err := mapper.Call(JSONString(doc), MakeMeta(docid, vbNo, vbSeq))
77-
if err != nil {
78-
return nil, err
79-
}
80-
rows := result1.([]*ViewRow)
81-
for i, _ := range rows {
82-
rows[i].ID = docid
34+
func (mapper *JSMapFunction) CallFunction(ctx context.Context, doc string, docid string, vbNo uint32, vbSeq uint64) ([]*ViewRow, error) {
35+
if mapper.timeout > 0 {
36+
newCtx, cancel := context.WithTimeout(ctx, mapper.timeout)
37+
ctx = newCtx
38+
defer cancel()
8339
}
84-
return rows, nil
85-
}
86-
87-
// Returns a Couchbase-compatible 'meta' object, given a document ID
88-
func MakeMeta(docid string, vbNo uint32, vbSeq uint64) map[string]interface{} {
89-
return map[string]interface{}{
90-
"id": docid,
91-
"vb": uint32(vbNo), // convert back to well known type
92-
"seq": uint64(vbSeq), // ditto
40+
if output, err := mapper.service.Run(ctx, doc, docid, vbNo, vbSeq); err != nil {
41+
return nil, err
42+
} else if output == nil {
43+
return nil, nil
44+
} else if jsRows, ok := output.(string); !ok {
45+
return nil, fmt.Errorf("JSMapFunction internal error: wrong type of result, %T", output)
46+
} else {
47+
var rows []*ViewRow
48+
err = json.Unmarshal([]byte(jsRows), &rows)
49+
return rows, err
9350
}
94-
9551
}

js_map_fn.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
function() { var mapFn = %s;
2+
// (put the interpolated map fn on line 1 so the line numbers in any syntax error will match)
3+
4+
// Copyright 2023-Present Couchbase, Inc.
5+
//
6+
// Use of this software is governed by the Business Source License included
7+
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
8+
// in that file, in accordance with the Business Source License, use of this
9+
// software will be governed by the Apache License, Version 2.0, included in
10+
// the file licenses/APL2.txt.
11+
12+
if (typeof(mapFn) !== 'function') {
13+
throw new Error('code does not compile to a function');
14+
} else if (mapFn.length < 1 || mapFn.length > 3) {
15+
throw new Error('map function must have 1-3 arguments');
16+
}
17+
18+
var curDocID = undefined;
19+
var result = undefined;
20+
21+
function emit(key, value) {
22+
var row = {key: key, value: value, id: curDocID}
23+
if (result === undefined) {
24+
result = [row];
25+
} else {
26+
result.push(row);
27+
}
28+
}
29+
30+
var log = console.log;
31+
32+
{
33+
/**** Variables used during the call but not visible to the map fn ****/
34+
35+
// For security reasons, prevent scripts from dynamically compiling code:
36+
delete eval;
37+
delete Function;
38+
39+
/**** The function that runs the map function ****/
40+
41+
return function (docJson, docid, vbNo, vbSeq) {
42+
var doc = JSON.parse(docJson);
43+
var meta = {
44+
id: docid,
45+
vb: vbNo,
46+
seq: vbSeq
47+
};
48+
curDocID = docid;
49+
50+
result = undefined;
51+
52+
mapFn(doc, meta);
53+
54+
return result ? JSON.stringify(result) : undefined;
55+
}
56+
}
57+
}()

js_map_fn_test.go

Lines changed: 58 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,82 +9,97 @@
99
package sgbucket
1010

1111
import (
12+
"context"
1213
"fmt"
1314
"testing"
1415

16+
"github.com/couchbase/sg-bucket/js"
1517
"github.com/stretchr/testify/assert"
1618
)
1719

1820
// Just verify that the calls to the emit() fn show up in the output.
1921
func TestEmitFunction(t *testing.T) {
20-
mapper := NewJSMapFunction(`function(doc) {emit("key", "value"); emit("k2","v2")}`, 0)
21-
rows, err := mapper.CallFunction(`{}`, "doc1", 0, 0)
22-
assertNoError(t, err, "CallFunction failed")
23-
assert.Equal(t, 2, len(rows))
24-
assert.Equal(t, &ViewRow{ID: "doc1", Key: "key", Value: "value"}, rows[0])
25-
assert.Equal(t, &ViewRow{ID: "doc1", Key: "k2", Value: "v2"}, rows[1])
22+
js.TestWithVMPools(t, 4, func(t *testing.T, pool *js.VMPool) {
23+
mapper := NewJSMapFunction(pool, `function(doc) {emit("key", "value"); emit("k2","v2")}`, 0)
24+
rows, err := mapper.CallFunction(pool.Context(), `{}`, "doc1", 0, 0)
25+
assertNoError(t, err, "CallFunction failed")
26+
assert.Equal(t, 2, len(rows))
27+
assert.Equal(t, &ViewRow{ID: "doc1", Key: "key", Value: "value"}, rows[0])
28+
assert.Equal(t, &ViewRow{ID: "doc1", Key: "k2", Value: "v2"}, rows[1])
29+
})
2630
}
2731

2832
func TestTimeout(t *testing.T) {
29-
mapper := NewJSMapFunction(`function(doc) {while(true) {}}`, 1)
30-
_, err := mapper.CallFunction(`{}`, "doc1", 0, 0)
31-
assert.ErrorIs(t, err, ErrJSTimeout)
33+
js.TestWithVMPools(t, 4, func(t *testing.T, pool *js.VMPool) {
34+
mapper := NewJSMapFunction(pool, `function(doc) {while(true) {}}`, 1)
35+
_, err := mapper.CallFunction(pool.Context(), `{}`, "doc1", 0, 0)
36+
assert.ErrorIs(t, err, context.DeadlineExceeded)
37+
})
3238
}
3339

34-
func testMap(t *testing.T, mapFn string, doc string) []*ViewRow {
35-
mapper := NewJSMapFunction(mapFn, 0)
36-
rows, err := mapper.CallFunction(doc, "doc1", 0, 0)
40+
func testMap(t *testing.T, host js.ServiceHost, mapFn string, doc string) []*ViewRow {
41+
mapper := NewJSMapFunction(host, mapFn, 0)
42+
rows, err := mapper.CallFunction(host.Context(), doc, "doc1", 0, 0)
3743
assertNoError(t, err, fmt.Sprintf("CallFunction failed on %s", doc))
3844
return rows
3945
}
4046

4147
// Now just make sure the input comes through intact
4248
func TestInputParse(t *testing.T) {
43-
rows := testMap(t, `function(doc) {emit(doc.key, doc.value);}`,
44-
`{"key": "k", "value": "v"}`)
45-
assert.Equal(t, 1, len(rows))
46-
assert.Equal(t, &ViewRow{ID: "doc1", Key: "k", Value: "v"}, rows[0])
49+
js.TestWithVMPools(t, 4, func(t *testing.T, pool *js.VMPool) {
50+
rows := testMap(t, pool, `function(doc) {emit(doc.key, doc.value);}`,
51+
`{"key": "k", "value": "v"}`)
52+
assert.Equal(t, 1, len(rows))
53+
assert.Equal(t, &ViewRow{ID: "doc1", Key: "k", Value: "v"}, rows[0])
54+
})
4755
}
4856

4957
// Test different types of keys/values:
5058
func TestKeyTypes(t *testing.T) {
51-
rows := testMap(t, `function(doc) {emit(doc.key, doc.value);}`,
52-
`{"ID": "doc1", "key": true, "value": false}`)
53-
assert.Equal(t, &ViewRow{ID: "doc1", Key: true, Value: false}, rows[0])
54-
rows = testMap(t, `function(doc) {emit(doc.key, doc.value);}`,
55-
`{"ID": "doc1", "key": null, "value": 0}`)
56-
assert.Equal(t, &ViewRow{ID: "doc1", Key: nil, Value: float64(0)}, rows[0])
57-
rows = testMap(t, `function(doc) {emit(doc.key, doc.value);}`,
58-
`{"ID": "doc1", "key": ["foo", 23, []], "value": [null]}`)
59-
assert.Equal(t, &ViewRow{
60-
ID: "doc1",
61-
Key: []interface{}{"foo", 23.0, []interface{}{}},
62-
Value: []interface{}{nil},
63-
}, rows[0])
64-
59+
js.TestWithVMPools(t, 4, func(t *testing.T, pool *js.VMPool) {
60+
rows := testMap(t, pool, `function(doc) {emit(doc.key, doc.value);}`,
61+
`{"ID": "doc1", "key": true, "value": false}`)
62+
assert.Equal(t, &ViewRow{ID: "doc1", Key: true, Value: false}, rows[0])
63+
rows = testMap(t, pool, `function(doc) {emit(doc.key, doc.value);}`,
64+
`{"ID": "doc1", "key": null, "value": 0}`)
65+
assert.Equal(t, &ViewRow{ID: "doc1", Key: nil, Value: float64(0)}, rows[0])
66+
rows = testMap(t, pool, `function(doc) {emit(doc.key, doc.value);}`,
67+
`{"ID": "doc1", "key": ["foo", 23, []], "value": [null]}`)
68+
assert.Equal(t, &ViewRow{
69+
ID: "doc1",
70+
Key: []interface{}{"foo", 23.0, []interface{}{}},
71+
Value: []interface{}{nil},
72+
}, rows[0])
73+
})
6574
}
6675

6776
// Empty/no-op map fn
6877
func TestEmptyJSMapFunction(t *testing.T) {
69-
mapper := NewJSMapFunction(`function(doc) {}`, 0)
70-
rows, err := mapper.CallFunction(`{"key": "k", "value": "v"}`, "doc1", 0, 0)
71-
assertNoError(t, err, "CallFunction failed")
72-
assert.Equal(t, 0, len(rows))
78+
js.TestWithVMPools(t, 4, func(t *testing.T, pool *js.VMPool) {
79+
mapper := NewJSMapFunction(pool, `function(doc) {}`, 0)
80+
rows, err := mapper.CallFunction(pool.Context(), `{"key": "k", "value": "v"}`, "doc1", 0, 0)
81+
assertNoError(t, err, "CallFunction failed")
82+
assert.Equal(t, 0, len(rows))
83+
})
7384
}
7485

7586
// Test meta object
7687
func TestMeta(t *testing.T) {
77-
mapper := NewJSMapFunction(`function(doc,meta) {if (meta.id!="doc1") throw("bad ID");}`, 0)
78-
rows, err := mapper.CallFunction(`{"key": "k", "value": "v"}`, "doc1", 0, 0)
79-
assertNoError(t, err, "CallFunction failed")
80-
assert.Equal(t, 0, len(rows))
88+
js.TestWithVMPools(t, 4, func(t *testing.T, pool *js.VMPool) {
89+
mapper := NewJSMapFunction(pool, `function(doc,meta) {if (meta.id!="doc1") throw("bad ID");}`, 0)
90+
rows, err := mapper.CallFunction(pool.Context(), `{"key": "k", "value": "v"}`, "doc1", 0, 0)
91+
assertNoError(t, err, "CallFunction failed")
92+
assert.Equal(t, 0, len(rows))
93+
})
8194
}
8295

8396
// Test the public API
8497
func TestPublicJSMapFunction(t *testing.T) {
85-
mapper := NewJSMapFunction(`function(doc) {emit(doc.key, doc.value);}`, 0)
86-
rows, err := mapper.CallFunction(`{"key": "k", "value": "v"}`, "doc1", 0, 0)
87-
assertNoError(t, err, "CallFunction failed")
88-
assert.Equal(t, 1, len(rows))
89-
assert.Equal(t, &ViewRow{ID: "doc1", Key: "k", Value: "v"}, rows[0])
98+
js.TestWithVMPools(t, 4, func(t *testing.T, pool *js.VMPool) {
99+
mapper := NewJSMapFunction(pool, `function(doc) {emit(doc.key, doc.value);}`, 0)
100+
rows, err := mapper.CallFunction(pool.Context(), `{"key": "k", "value": "v"}`, "doc1", 0, 0)
101+
assertNoError(t, err, "CallFunction failed")
102+
assert.Equal(t, 1, len(rows))
103+
assert.Equal(t, &ViewRow{ID: "doc1", Key: "k", Value: "v"}, rows[0])
104+
})
90105
}

0 commit comments

Comments
 (0)