1
- defmodule Mongo.Cursor do
2
- @ moduledoc """
3
- MongoDB Cursor as a stream. There are two variants:
4
- * normal cursor: This is called in batch mode and closes automatically with a kill cursor command.
5
- * change stream cursor: This will operate a change stream. MongoDB does not return documents after the time has expired. In this case
6
- `get_more` will called again. No kill cursor command is invoked just because no documents are being returned. In case of error
7
- a resume process is started so that events can be received again without losing any previous events.
8
- The resume process requires a resume token or an operation time. These are cached by the cursor. One can
9
- determine the resume token via a function (on_resume_token). It is called when the resume token changed.
10
- """
1
+ defmodule Mongo.ChangeStream do
11
2
12
3
alias Mongo.Session
13
- alias Mongo.Cursor
14
4
15
5
import Record , only: [ defrecordp: 2 ]
16
6
17
- @ type t :: % __MODULE__ {
18
- topology_pid: GenServer . server ,
19
- cmd: BSON . document ,
20
- on_resume_token: fun ,
21
- opts: Keyword . t
22
- }
23
-
24
- defstruct [ :topology_pid , :cmd , :on_resume_token , :opts ]
25
-
7
+ defstruct [ :topology_pid , :session , :doc , :cmd , :on_resume_token , :opts ]
8
+
9
+ def new ( topology_pid , cmd , on_resume_token_fun , opts ) do
10
+ with new_cmd = Mongo.ReadPreference . add_read_preference ( cmd , opts ) ,
11
+ { :ok , session } <- Session . start_implicit_session ( topology_pid , :read , opts ) ,
12
+ { :ok , % { "ok" => ok } = doc } when ok == 1 <- Mongo . exec_command_session ( session , new_cmd , opts ) do
13
+ % Mongo.ChangeStream {
14
+ topology_pid: topology_pid ,
15
+ session: session ,
16
+ doc: doc ,
17
+ on_resume_token: on_resume_token_fun ,
18
+ cmd: cmd ,
19
+ opts: opts
20
+ }
21
+ end
22
+ end
26
23
defimpl Enumerable do
27
24
28
25
defrecordp :change_stream , [ :resume_token , :op_time , :cmd , :on_resume_token ]
29
26
defrecordp :state , [ :topology_pid , :session , :cursor , :coll , :change_stream , :docs ]
30
27
31
- def reduce ( % Cursor { topology_pid: topology_pid , cmd: cmd , on_resume_token: on_resume_token_fun , opts: opts } , acc , reduce_fun ) do
32
-
33
- start_fun = start_fun ( topology_pid , cmd , on_resume_token_fun , opts )
34
- next_fun = next_fun ( opts )
35
- after_fun = after_fun ( opts )
28
+ def reduce ( change_stream , acc , reduce_fun ) do
36
29
37
- Stream . resource ( start_fun , next_fun , after_fun ) . ( acc , reduce_fun )
38
- end
39
-
40
- ##
41
- # start of a regular cursor
42
- #
43
- defp start_fun ( topology_pid , cmd , nil , opts ) do
44
- fn ->
45
-
46
- with cmd = Mongo.ReadPreference . add_read_preference ( cmd , opts ) ,
47
- { :ok , session } <- Session . start_implicit_session ( topology_pid , :read , opts ) ,
48
- { :ok ,
49
- % { "ok" => ok ,
50
- "cursor" => % {
51
- "id" => cursor_id ,
52
- "ns" => coll ,
53
- "firstBatch" => docs } } } when ok == 1 <- Mongo . exec_command_session ( session , cmd , opts ) do
54
- state ( topology_pid: topology_pid , session: session , cursor: cursor_id , coll: coll , docs: docs )
55
- end
56
-
57
- end
58
- end
59
-
60
- ##
61
- # start of a change stream cursor
62
- #
63
- defp start_fun ( topology_pid , cmd , fun , opts ) do
64
- fn ->
65
- with { :ok , state } <- aggregate ( topology_pid , cmd , fun , opts ) do
30
+ start_fun = fn ->
31
+ with { :ok , state } <- aggregate ( change_stream . topology_pid , change_stream . session , change_stream . doc , change_stream . cmd , change_stream . on_resume_token ) do
66
32
state
67
- end
33
+ end
68
34
end
35
+ next_fun = next_fun ( change_stream . opts )
36
+ after_fun = after_fun ( change_stream . opts )
37
+
38
+ Stream . resource ( start_fun , next_fun , after_fun ) . ( acc , reduce_fun )
69
39
end
70
40
71
41
defp next_fun ( opts ) do
72
42
fn
73
43
state ( docs: [ ] , cursor: 0 ) = state -> { :halt , state }
74
44
75
- # this is a regular cursor
76
- state ( docs: [ ] , topology_pid: topology_pid , session: session , cursor: cursor , change_stream: nil , coll: coll ) = state ->
77
- case get_more ( topology_pid , session , only_coll ( coll ) , cursor , nil , opts ) do
78
- { :ok , % { cursor_id: cursor_id , docs: [ ] } } -> { :halt , state ( state , cursor: cursor_id ) }
79
- { :ok , % { cursor_id: cursor_id , docs: docs } } -> { docs , state ( state , cursor: cursor_id ) }
80
- { :error , error } -> raise error
81
- end
82
-
83
- # this is a change stream cursor
84
45
state ( docs: [ ] , topology_pid: topology_pid , session: session , cursor: cursor , change_stream: change_stream , coll: coll ) = state ->
85
46
case get_more ( topology_pid , session , only_coll ( coll ) , cursor , change_stream , opts ) do
86
47
{ :ok , % { cursor_id: cursor_id ,
87
- docs: docs ,
88
- change_stream: change_stream } } -> { docs , state ( state , cursor: cursor_id , change_stream: change_stream ) }
48
+ docs: docs ,
49
+ change_stream: change_stream } } -> { docs , state ( state , cursor: cursor_id , change_stream: change_stream ) }
89
50
{ :resume , state ( docs: docs ) = state } -> { docs , state ( state , docs: [ ] ) }
90
51
{ :error , error } -> raise error
91
52
end
@@ -99,63 +60,55 @@ defmodule Mongo.Cursor do
99
60
100
61
with new_cmd = Mongo.ReadPreference . add_read_preference ( cmd , opts ) ,
101
62
{ :ok , session } <- Session . start_implicit_session ( topology_pid , :read , opts ) ,
102
- { :ok , % { "ok" => ok ,
103
- "operationTime" => op_time ,
63
+ { :ok , % { "ok" => ok } = doc } when ok == 1 <- Mongo . exec_command_session ( session , new_cmd , opts ) do
64
+
65
+ aggregate ( topology_pid , session , doc , cmd , fun )
66
+ end
67
+ end
68
+
69
+ def aggregate ( topology_pid , session , doc , cmd , fun ) do
70
+
71
+ with % { "operationTime" => op_time ,
104
72
"cursor" => % {
105
73
"id" => cursor_id ,
106
74
"ns" => coll ,
107
- "firstBatch" => docs } = response } } when ok == 1 <- Mongo . exec_command_session ( session , new_cmd , opts ) ,
75
+ "firstBatch" => docs } = response } <- doc ,
108
76
{ :ok , wire_version } <- Mongo . wire_version ( topology_pid ) do
109
77
110
- [ % { "$changeStream" => stream_opts } | _pipeline ] = Keyword . get ( new_cmd , :pipeline ) # extract the change stream options
78
+ [ % { "$changeStream" => stream_opts } | _pipeline ] = Keyword . get ( cmd , :pipeline ) # extract the change stream options
111
79
112
- # The ChangeStream MUST save the operationTime from the initial aggregate response when the following critera are met:
113
- #
114
- # None of startAtOperationTime, resumeAfter, startAfter were specified in the ChangeStreamOptions.
115
- # The max wire version is >= 7.
116
- # The initial aggregate response had no results.
117
- # The initial aggregate response did not include a postBatchResumeToken.
80
+ # The ChangeStream MUST save the operationTime from the initial aggregate response when the following critera are met:
81
+ #
82
+ # None of startAtOperationTime, resumeAfter, startAfter were specified in the ChangeStreamOptions.
83
+ # The max wire version is >= 7.
84
+ # The initial aggregate response had no results.
85
+ # The initial aggregate response did not include a postBatchResumeToken.
118
86
119
- has_values = stream_opts [ "startAtOperationTime" ] || stream_opts [ "startAfter" ] || stream_opts [ "resumeAfter" ]
120
- op_time = update_operation_time ( op_time , has_values , docs , response [ "postBatchResumeToken" ] , wire_version )
87
+ has_values = stream_opts [ "startAtOperationTime" ] || stream_opts [ "startAfter" ] || stream_opts [ "resumeAfter" ]
88
+ op_time = update_operation_time ( op_time , has_values , docs , response [ "postBatchResumeToken" ] , wire_version )
121
89
122
- # When the ChangeStream is started:
123
- # If startAfter is set, cache it.
124
- # Else if resumeAfter is set, cache it.
125
- # Else, resumeToken remains unset.
126
- resume_token = stream_opts [ "startAfter" ] || stream_opts [ "resumeAfter" ]
127
- resume_token = update_resume_token ( resume_token , response [ "postBatchResumeToken" ] , List . last ( docs ) )
90
+ # When the ChangeStream is started:
91
+ # If startAfter is set, cache it.
92
+ # Else if resumeAfter is set, cache it.
93
+ # Else, resumeToken remains unset.
94
+ resume_token = stream_opts [ "startAfter" ] || stream_opts [ "resumeAfter" ]
95
+ resume_token = update_resume_token ( resume_token , response [ "postBatchResumeToken" ] , List . last ( docs ) )
128
96
129
- fun . ( resume_token )
97
+ fun . ( resume_token )
130
98
131
- change_stream = change_stream ( resume_token: resume_token , op_time: op_time , cmd: cmd , on_resume_token: fun )
99
+ change_stream = change_stream ( resume_token: resume_token , op_time: op_time , cmd: cmd , on_resume_token: fun )
132
100
133
- { :ok , state ( topology_pid: topology_pid , session: session , cursor: cursor_id , coll: coll , change_stream: change_stream , docs: docs ) }
101
+ { :ok , state ( topology_pid: topology_pid , session: session , cursor: cursor_id , coll: coll , change_stream: change_stream , docs: docs ) }
134
102
end
135
103
end
136
104
137
105
@ doc """
138
106
Calls the GetCore-Command
139
107
See https://github.com/mongodb/specifications/blob/master/source/find_getmore_killcursors_commands.rst
140
108
"""
141
- def get_more ( _topology_pid , session , coll , cursor , nil , opts ) do
142
-
143
- cmd = [
144
- getMore: % BSON.LongNumber { value: cursor } ,
145
- collection: coll ,
146
- batchSize: opts [ :batch_size ] ,
147
- maxTimeMS: opts [ :max_time ]
148
- ] |> filter_nils ( )
149
-
150
- with { :ok , % { "cursor" => % { "id" => cursor_id , "nextBatch" => docs } , "ok" => ok } } when ok == 1 <- Mongo . exec_command_session ( session , cmd , opts ) do
151
- { :ok , % { cursor_id: cursor_id , docs: docs } }
152
- end
153
-
154
- end
155
-
156
109
def get_more ( topology_pid , session , coll , cursor_id ,
157
- change_stream ( resume_token: resume_token , op_time: op_time , cmd: aggregate_cmd ,
158
- on_resume_token: fun ) = change_stream , opts ) do
110
+ change_stream ( resume_token: resume_token , op_time: op_time , cmd: aggregate_cmd ,
111
+ on_resume_token: fun ) = change_stream , opts ) do
159
112
160
113
get_more = [
161
114
getMore: % BSON.LongNumber { value: cursor_id } ,
@@ -165,9 +118,9 @@ defmodule Mongo.Cursor do
165
118
] |> filter_nils ( )
166
119
167
120
with { :ok , % { "operationTime" => op_time ,
168
- "cursor" => % { "id" => new_cursor_id ,
169
- "nextBatch" => docs } = cursor ,
170
- "ok" => ok } } when ok == 1 <- Mongo . exec_command_session ( session , get_more , opts ) do
121
+ "cursor" => % { "id" => new_cursor_id ,
122
+ "nextBatch" => docs } = cursor ,
123
+ "ok" => ok } } when ok == 1 <- Mongo . exec_command_session ( session , get_more , opts ) do
171
124
172
125
old_token = change_stream ( change_stream , :resume_token )
173
126
change_stream = update_change_stream ( change_stream , cursor [ "postBatchResumeToken" ] , op_time , List . last ( docs ) )
@@ -282,14 +235,14 @@ defmodule Mongo.Cursor do
282
235
def kill_cursors ( session , coll , cursor_ids , opts ) do
283
236
284
237
cmd = [
285
- killCursors: coll ,
286
- cursors: cursor_ids |> Enum . map ( fn id -> % BSON.LongNumber { value: id } end )
287
- ] |> filter_nils ( )
238
+ killCursors: coll ,
239
+ cursors: cursor_ids |> Enum . map ( fn id -> % BSON.LongNumber { value: id } end )
240
+ ] |> filter_nils ( )
288
241
289
242
with { :ok , % { "cursorsAlive" => [ ] ,
290
- "cursorsNotFound" => [ ] ,
291
- "cursorsUnknown" => [ ] ,
292
- "ok" => ok } } when ok == 1 <- Mongo . exec_command_session ( session , cmd , opts ) do
243
+ "cursorsNotFound" => [ ] ,
244
+ "cursorsUnknown" => [ ] ,
245
+ "ok" => ok } } when ok == 1 <- Mongo . exec_command_session ( session , cmd , opts ) do
293
246
:ok
294
247
end
295
248
end
@@ -320,6 +273,5 @@ defmodule Mongo.Cursor do
320
273
def count ( _stream ) , do: { :error , __MODULE__ }
321
274
def member? ( _stream , _term ) , do: { :error , __MODULE__ }
322
275
323
-
324
276
end
325
- end
277
+ end
0 commit comments