libcouchbase 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -4
  3. data/README.md +4 -0
  4. data/ext/libcouchbase/CMakeLists.txt +1 -1
  5. data/ext/libcouchbase/RELEASE_NOTES.markdown +42 -0
  6. data/ext/libcouchbase/cmake/Modules/GetVersionInfo.cmake +3 -3
  7. data/ext/libcouchbase/cmake/source_files.cmake +1 -0
  8. data/ext/libcouchbase/include/libcouchbase/cntl.h +27 -1
  9. data/ext/libcouchbase/include/libcouchbase/couchbase.h +0 -10
  10. data/ext/libcouchbase/include/libcouchbase/error.h +8 -1
  11. data/ext/libcouchbase/include/memcached/protocol_binary.h +12 -3
  12. data/ext/libcouchbase/src/auth.cc +0 -4
  13. data/ext/libcouchbase/src/cntl.cc +11 -1
  14. data/ext/libcouchbase/src/connspec.cc +18 -0
  15. data/ext/libcouchbase/src/connspec.h +10 -0
  16. data/ext/libcouchbase/src/dns-srv.cc +13 -14
  17. data/ext/libcouchbase/src/errmap.cc +107 -0
  18. data/ext/libcouchbase/src/errmap.h +113 -0
  19. data/ext/libcouchbase/src/hostlist.cc +0 -35
  20. data/ext/libcouchbase/src/hostlist.h +38 -64
  21. data/ext/libcouchbase/src/http/http.cc +6 -1
  22. data/ext/libcouchbase/src/instance.cc +1 -1
  23. data/ext/libcouchbase/src/internal.h +10 -0
  24. data/ext/libcouchbase/src/mcserver/mcserver.cc +119 -3
  25. data/ext/libcouchbase/src/mcserver/mcserver.h +3 -1
  26. data/ext/libcouchbase/src/mcserver/negotiate.cc +130 -37
  27. data/ext/libcouchbase/src/nodeinfo.cc +1 -1
  28. data/ext/libcouchbase/src/settings.c +3 -0
  29. data/ext/libcouchbase/src/settings.h +5 -0
  30. data/ext/libcouchbase/src/ssl/ssl_common.c +2 -0
  31. data/ext/libcouchbase/tests/basic/t_host.cc +67 -75
  32. data/ext/libcouchbase/tests/iotests/mock-environment.h +2 -1
  33. data/ext/libcouchbase/tests/iotests/t_confmon.cc +3 -4
  34. data/ext/libcouchbase/tests/iotests/t_errmap.cc +97 -0
  35. data/lib/libcouchbase/bucket.rb +27 -12
  36. data/lib/libcouchbase/callbacks.rb +1 -1
  37. data/lib/libcouchbase/connection.rb +18 -5
  38. data/lib/libcouchbase/version.rb +1 -1
  39. data/spec/connection_spec.rb +1 -1
  40. metadata +5 -2
@@ -0,0 +1,113 @@
1
+ #ifndef LCB_ERRMAP_H
2
+ #define LCB_ERRMAP_H
3
+
4
+ #ifdef __cplusplus
5
+
6
+ #include <map>
7
+ #include <set>
8
+ #include <string>
9
+
10
+ namespace lcb {
11
+ namespace errmap {
12
+
13
+ enum ErrorAttribute {
14
+ #define LCB_XERRMAP_ATTRIBUTES(X) \
15
+ X(TEMPORARY, "temp") \
16
+ X(SUBDOC, "subdoc") \
17
+ X(RETRY_NOW, "retry-now") \
18
+ X(RETRY_LATER, "retry-later") \
19
+ X(INVALID_INPUT, "invalid-input") \
20
+ X(NOT_ENABLED, "support") \
21
+ X(AUTH, "auth") \
22
+ X(CONN_STATE_INVALIDATED, "conn-state-invalidated") \
23
+ X(CONSTRAINT_FAILURE, "item-only") \
24
+ X(RETRY_EXP_BACKOFF, "retry-exp-backoff") \
25
+ X(RETRY_LINEAR_BACKOFF, "retry-linear-backoff") \
26
+ X(INTERNAL, "internal") \
27
+ X(DCP, "dcp") \
28
+ X(FETCH_CONFIG, "fetch-config") \
29
+ X(SPECIAL_HANDLING, "special-handling")
30
+
31
+ #define X(c, s) c,
32
+ LCB_XERRMAP_ATTRIBUTES(X)
33
+ #undef X
34
+
35
+ INVALID_ATTRIBUTE
36
+ };
37
+
38
+ struct Error {
39
+ uint16_t code;
40
+ std::string shortname;
41
+ std::string description;
42
+ std::set<ErrorAttribute> attributes;
43
+
44
+ Error() : code(-1) {
45
+ }
46
+
47
+ bool isValid() const {
48
+ return code != uint16_t(-1);
49
+ }
50
+
51
+ bool hasAttribute(ErrorAttribute attr) const {
52
+ return attributes.find(attr) != attributes.end();
53
+ }
54
+ };
55
+
56
+ class ErrorMap {
57
+ public:
58
+ enum ParseStatus {
59
+ /** Couldn't parse JSON!*/
60
+ PARSE_ERROR,
61
+
62
+ /** Version is too high */
63
+ UNKNOWN_VERSION,
64
+
65
+ /** Current version/revision is higher or equal */
66
+ NOT_UPDATED,
67
+
68
+ /** Updated */
69
+ UPDATED
70
+ };
71
+
72
+ ErrorMap();
73
+ ParseStatus parse(const char *s, size_t n, std::string& errmsg);
74
+ ParseStatus parse(const char *s, size_t n) {
75
+ std::string tmp;
76
+ return parse(s, n, tmp);
77
+ }
78
+ size_t getVersion() const { return version; }
79
+ size_t getRevision() const { return revision; };
80
+ const Error& getError(uint16_t code) const;
81
+ bool isLoaded() const {
82
+ return !errors.empty();
83
+ }
84
+
85
+ private:
86
+ static const uint32_t MAX_VERSION;
87
+ ErrorMap(const ErrorMap&);
88
+ typedef std::map<uint16_t, Error> MapType;
89
+ MapType errors;
90
+ uint32_t revision;
91
+ uint32_t version;
92
+ };
93
+
94
+ } // namespace
95
+ } // namespace
96
+
97
+ typedef lcb::errmap::ErrorMap* lcb_pERRMAP;
98
+ #else
99
+ typedef struct lcb_ERRMAP* lcb_pERRMAP;
100
+ #endif /* __cplusplus */
101
+
102
+ #ifdef __cplusplus
103
+ extern "C" {
104
+ #endif
105
+
106
+ lcb_pERRMAP lcb_errmap_new(void);
107
+ void lcb_errmap_free(lcb_pERRMAP);
108
+
109
+ #ifdef __cplusplus
110
+ }
111
+ #endif
112
+
113
+ #endif /* LCB_ERRMAP_H */
@@ -263,38 +263,3 @@ Hostlist::assign(const Hostlist& src)
263
263
  }
264
264
  return *this;
265
265
  }
266
-
267
- extern "C" {
268
- Hostlist* hostlist_create(void) { return new Hostlist(); }
269
- void hostlist_destroy(hostlist_t l) { delete l; }
270
- void hostlist_clear(hostlist_t l) { l->clear(); }
271
- void hostlist_reset_strlist(hostlist_t l) { l->reset_strlist(); }
272
- lcb_error_t hostlist_add_host(hostlist_t l, const lcb_host_t *h) { l->add(*h); return LCB_SUCCESS; }
273
- lcb_host_t *hostlist_shift_next(hostlist_t hl, int wrap) { return hl->next(wrap); }
274
- int hostlist_finished(hostlist_t l) { return l->ix == l->hosts.size(); }
275
- size_t hostlist_size(hostlist_t l) { return l->size(); }
276
- void hostlist_randomize(hostlist_t l) { l->randomize(); }
277
-
278
- lcb_error_t
279
- hostlist_add_string(hostlist_t hl, const char *spec, int len, int deflport) {
280
- return hl->add(spec, len, deflport);
281
- }
282
-
283
- void
284
- hostlist_assign(hostlist_t dst, const hostlist_t src) {
285
- dst->assign(*src);
286
- }
287
-
288
- const lcb_host_t*
289
- hostlist_get(const hostlist_t h, size_t ix) { return &h->hosts[ix]; }
290
-
291
- const char * const *
292
- hostlist_strents(const hostlist_t h) {
293
- h->ensure_strlist();
294
- if (h->hoststrs.size()) {
295
- return &h->hoststrs[0];
296
- } else {
297
- return NULL;
298
- }
299
- }
300
- }
@@ -45,56 +45,74 @@ struct Hostlist {
45
45
  Hostlist() : ix(0) {}
46
46
  ~Hostlist();
47
47
 
48
- void add(const lcb_host_t&);
48
+ /**
49
+ * Adds a string to the hostlist. See lcb_host_parse for details.
50
+ * Note that if the host already exists (see 'lcb_host_equals') it will
51
+ * not be added
52
+ * @param s the string to parse
53
+ * @param len the length of the string
54
+ * @param deflport If `s` does not contain an explicit port, use this
55
+ * port instead.
56
+ * @return LCB_EINVAL if the host string is not valid
57
+ */
49
58
  lcb_error_t add(const char *s, long len, int deflport);
50
59
  lcb_error_t add(const char *s, int deflport) { return add(s, -1, deflport); }
60
+ void add(const lcb_host_t&);
61
+
51
62
  bool exists(const lcb_host_t&) const;
52
63
  bool exists(const char *hostport) const;
64
+
65
+ /**
66
+ * Return the next host in the list.
67
+ * @param wrap If the internal iterator has reached its limit, this
68
+ * indicates whether it should be reset, or if it should return NULL
69
+ * @return a new host if available, or NULL if the list is empty or the
70
+ * iterator is finished.
71
+ */
53
72
  lcb_host_t *next(bool wrap);
54
73
  bool finished() const;
55
74
 
56
75
  size_t size() const { return hosts.size(); }
57
76
  bool empty() const { return hosts.empty(); }
58
77
  Hostlist& assign(const Hostlist& other);
78
+
79
+ /** Clears the hostlist */
59
80
  void clear() { hosts.clear(); reset_strlist(); ix = 0; }
81
+
82
+ /** Randomize the hostlist by shuffling the order. */
60
83
  void randomize();
84
+
85
+ /**
86
+ * String list handling functions. These are used to return the hostlist via
87
+ * the API where we return a char*[] terminated by a NULL pointer.
88
+ */
89
+
90
+ /** Ensure that the string list contains at least one entry */
61
91
  void ensure_strlist();
92
+
93
+ /** Frees the current list of strings */
62
94
  void reset_strlist();
95
+
96
+ const char * const *get_strlist() const { return &hoststrs[0]; }
97
+
63
98
  unsigned int ix;
64
99
  const lcb_host_t& operator[](size_t ix_) const { return hosts[ix_]; }
65
100
 
66
101
  std::vector<lcb_host_t> hosts;
67
102
  std::vector<const char *> hoststrs;
68
- static Hostlist* from_c(hostlist_st* src) {
69
- return reinterpret_cast<Hostlist*>(src);
70
- }
71
103
  };
72
104
  }
73
105
  typedef lcb::Hostlist* hostlist_t;
74
106
 
75
107
  struct hostlist_st : lcb::Hostlist {
108
+ hostlist_st() : Hostlist() {
109
+ }
76
110
  };
77
111
  #endif
78
112
 
79
113
  #ifdef __cplusplus
80
114
  extern "C" {
81
115
  #endif
82
-
83
- /**
84
- * Creates a new hostlist. Returns NULL on allocation failure
85
- */
86
- hostlist_t hostlist_create(void);
87
-
88
- /**
89
- * Frees resources associated with the hostlist
90
- */
91
- void hostlist_destroy(hostlist_t hostlist);
92
-
93
- /**
94
- * Clears the hostlist
95
- */
96
- void hostlist_clear(hostlist_t hostlist);
97
-
98
116
  /**
99
117
  * Parses a string into a hostlist
100
118
  * @param host the target host to populate
@@ -126,50 +144,6 @@ lcb_host_parse(lcb_host_t *host, const char *spec, int speclen, int deflport);
126
144
  */
127
145
  int lcb_host_equals(const lcb_host_t *a, const lcb_host_t *b);
128
146
 
129
- /**
130
- * Adds a string to the hostlist. See lcb_host_parse for details.
131
- * Note that if the host already exists (see 'lcb_host_equals') it will
132
- * not be added
133
- * @return LCB_EINVAL if the host string is not value, LCB_CLIENT_ENOMEM on
134
- * allocation failure.
135
- */
136
- lcb_error_t hostlist_add_string(hostlist_t hostlist,
137
- const char *spec,
138
- int speclen,
139
- int deflport);
140
-
141
- #define hostlist_add_stringz(hostlist, spec, deflport) \
142
- hostlist_add_string(hostlist, spec, -1, deflport)
143
-
144
- lcb_error_t hostlist_add_host(hostlist_t hostlist, const lcb_host_t *host);
145
-
146
- /**
147
- * Return the next host in the list.
148
- * @param hostlist the hostlist to use
149
- * @param rollover If the internal iterator has reached its limit, this
150
- * indicates whether it should be reset, or if it should return NULL
151
- * @return a new host if available, or NULL if the list is empty or the
152
- * iterator is finished.
153
- */
154
- lcb_host_t *hostlist_shift_next(hostlist_t hostlist, int rollover);
155
-
156
- /**
157
- * Randomize the hostlist
158
- */
159
- void hostlist_randomize(hostlist_t hostlist);
160
-
161
- /**
162
- * String list handling functions. These are used to return the hostlist via
163
- * the API where we return a char*[] terminated by a NULL pointer.
164
- */
165
- void hostlist_reset_strlist(hostlist_t hostlist);
166
-
167
- /** Whether the internal iterator has finished. */
168
- int hostlist_finished(hostlist_t);
169
- size_t hostlist_size(const hostlist_t hl);
170
- void hostlist_assign(hostlist_t dst, const hostlist_t src);
171
- const lcb_host_t *hostlist_get(const hostlist_t, size_t);
172
- const char * const *hostlist_strents(const hostlist_t hl);
173
147
  #ifdef __cplusplus
174
148
  }
175
149
  #endif
@@ -459,7 +459,12 @@ Request::setup_inputs(const lcb_CMDHTTP *cmd)
459
459
  return rc;
460
460
  }
461
461
 
462
- add_header("User-Agent", "libcouchbase/" LCB_VERSION_STRING);
462
+ std::string ua("libcouchbase/" LCB_VERSION_STRING);
463
+ if (instance->settings->client_string) {
464
+ ua.append(" ").append(instance->settings->client_string);
465
+ }
466
+ add_header("User-Agent", ua);
467
+
463
468
  if (instance->http_sockpool->maxidle == 0 || !is_data_request()) {
464
469
  add_header("Connection", "close");
465
470
  }
@@ -129,7 +129,7 @@ lcb_st::process_dns_srv(Connspec& spec)
129
129
 
130
130
  const Spechost& host = spec.hosts().front();
131
131
  lcb_error_t rc = LCB_ERROR;
132
- Hostlist* hl = lcb_dnssrv_getbslist(host.hostname.c_str(), host.isSSL(), &rc);
132
+ Hostlist* hl = dnssrv_getbslist(host.hostname.c_str(), host.isSSL(), rc);
133
133
 
134
134
  if (hl == NULL) {
135
135
  lcb_log(LOGARGS(this, INFO), "DNS SRV lookup failed: %s. Ignore this if not relying on DNS SRV records", lcb_strerror(this, rc));
@@ -165,6 +165,16 @@ struct lcb_st {
165
165
  }
166
166
  return bs_state->bootstrap(options);
167
167
  }
168
+
169
+ lcbvb_CONFIG *getConfig() const {
170
+ return cur_configinfo->vbc;
171
+ }
172
+
173
+ int map_key(const std::string& key) {
174
+ int srvix, tmpvb;
175
+ lcbvb_map_key(getConfig(), key.c_str(), key.size(), &tmpvb, &srvix);
176
+ return srvix;
177
+ }
168
178
  #endif
169
179
  };
170
180
 
@@ -29,6 +29,8 @@
29
29
  #define LOGARGS_T(lvl) LOGARGS(this, lvl)
30
30
 
31
31
  #define LOGFMT "<%s:%s> (SRV=%p,IX=%d) "
32
+ #define PKTFMT "OP=0x%x, RC=0x%x, SEQ=%u"
33
+ #define PKTARGS(pkt) (pkt).opcode(), (pkt).status(), (pkt).opaque()
32
34
 
33
35
  #define LOGID(server) get_ctx_host(server->connctx), get_ctx_port(server->connctx), (void*)server, server->index
34
36
  #define LOGID_T() LOGID(this)
@@ -164,6 +166,107 @@ Server::handle_nmv(MemcachedResponse& resinfo, mc_PACKET *oldpkt)
164
166
  return true;
165
167
  }
166
168
 
169
+ /**
170
+ * Determine if this is an error code that we can pass to the user, or can
171
+ * otherwise handle "innately"
172
+ */
173
+ static bool is_fastpath_error(uint16_t rc) {
174
+ switch (rc) {
175
+ case PROTOCOL_BINARY_RESPONSE_SUCCESS:
176
+ case PROTOCOL_BINARY_RESPONSE_KEY_ENOENT:
177
+ case PROTOCOL_BINARY_RESPONSE_KEY_EEXISTS:
178
+ case PROTOCOL_BINARY_RESPONSE_E2BIG:
179
+ case PROTOCOL_BINARY_RESPONSE_NOT_STORED:
180
+ case PROTOCOL_BINARY_RESPONSE_DELTA_BADVAL:
181
+ case PROTOCOL_BINARY_RESPONSE_ERANGE:
182
+ case PROTOCOL_BINARY_RESPONSE_NOT_SUPPORTED:
183
+ case PROTOCOL_BINARY_RESPONSE_UNKNOWN_COMMAND:
184
+ case PROTOCOL_BINARY_RESPONSE_ETMPFAIL:
185
+ case PROTOCOL_BINARY_RESPONSE_ENOMEM:
186
+ case PROTOCOL_BINARY_RESPONSE_SUBDOC_PATH_ENOENT:
187
+ case PROTOCOL_BINARY_RESPONSE_SUBDOC_PATH_EEXISTS:
188
+ case PROTOCOL_BINARY_RESPONSE_SUBDOC_PATH_MISMATCH:
189
+ case PROTOCOL_BINARY_RESPONSE_SUBDOC_PATH_EINVAL:
190
+ case PROTOCOL_BINARY_RESPONSE_SUBDOC_PATH_E2BIG:
191
+ case PROTOCOL_BINARY_RESPONSE_SUBDOC_VALUE_CANTINSERT:
192
+ case PROTOCOL_BINARY_RESPONSE_SUBDOC_VALUE_ETOODEEP:
193
+ case PROTOCOL_BINARY_RESPONSE_SUBDOC_DOC_NOTJSON:
194
+ case PROTOCOL_BINARY_RESPONSE_SUBDOC_NUM_ERANGE:
195
+ case PROTOCOL_BINARY_RESPONSE_SUBDOC_DELTA_ERANGE:
196
+ case PROTOCOL_BINARY_RESPONSE_SUBDOC_INVALID_COMBO:
197
+ case PROTOCOL_BINARY_RESPONSE_SUBDOC_MULTI_PATH_FAILURE:
198
+ return true;
199
+ default:
200
+ if (rc >= 0xc0 && rc <= 0xcc) {
201
+ // other subdoc?
202
+ return true;
203
+ } else {
204
+ return false;
205
+ }
206
+ break;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Handle an unknown memcached error
212
+ *
213
+ * @param mcresp Response which contains the unknown error
214
+ * @param[out] newerr more user-friendly based on error map attributes
215
+ *
216
+ * @return true if this function handled the error specially (by disconnecting)
217
+ * or false if normal handling should continue.
218
+ */
219
+ bool Server::handle_unknown_error(const MemcachedResponse& mcresp,
220
+ lcb_error_t& newerr) {
221
+
222
+ if (!settings->errmap->isLoaded() || !settings->use_errmap) {
223
+ // If there's no error map, just return false
224
+ return false;
225
+ }
226
+
227
+ // Look up the error map definition for this error
228
+ const errmap::Error& err = settings->errmap->getError(mcresp.status());
229
+
230
+ if (!err.isValid() || err.hasAttribute(errmap::SPECIAL_HANDLING)) {
231
+ lcb_log(LOGARGS_T(ERR), LOGFMT "Received error not in error map or requires special handling! " PKTFMT, LOGID_T(), PKTARGS(mcresp));
232
+ lcbio_ctx_senderr(connctx, LCB_PROTOCOL_ERROR);
233
+ return true;
234
+ } else {
235
+ lcb_log(LOGARGS_T(WARN), LOGFMT "Received server error %s (0x%x) on packet: " PKTFMT, LOGID_T(), err.shortname.c_str(), err.code, PKTARGS(mcresp));
236
+ }
237
+
238
+ if (err.hasAttribute(errmap::FETCH_CONFIG)) {
239
+ instance->bootstrap(BS_REFRESH_THROTTLE);
240
+ }
241
+
242
+ if (err.hasAttribute(errmap::TEMPORARY)) {
243
+ newerr = LCB_GENERIC_TMPERR;
244
+ }
245
+
246
+ if (err.hasAttribute(errmap::CONSTRAINT_FAILURE)) {
247
+ newerr = LCB_GENERIC_CONSTRAINT_ERR;
248
+ }
249
+
250
+ if (err.hasAttribute(errmap::AUTH)) {
251
+ newerr = LCB_AUTH_ERROR;
252
+ }
253
+
254
+ if (err.hasAttribute(errmap::SUBDOC) && newerr == LCB_SUCCESS) {
255
+ newerr = LCB_GENERIC_SUBDOCERR;
256
+ }
257
+
258
+ if (err.hasAttribute(errmap::CONN_STATE_INVALIDATED)) {
259
+ if (newerr != LCB_SUCCESS) {
260
+ newerr = LCB_ERROR;
261
+ }
262
+ lcbio_ctx_senderr(connctx, newerr);
263
+ return true;
264
+ }
265
+
266
+ return false;
267
+
268
+ }
269
+
167
270
  /* This function is called within a loop to process a single packet.
168
271
  *
169
272
  * If a full packet is available, it will process the packet and return
@@ -224,7 +327,14 @@ Server::try_read(lcbio_CTX *ctx, rdb_IOROPE *ior)
224
327
  return PKT_READ_COMPLETE;
225
328
  }
226
329
 
227
- if (mcresp.status() == PROTOCOL_BINARY_RESPONSE_NOT_MY_VBUCKET) {
330
+ lcb_error_t err_override = LCB_SUCCESS;
331
+ ReadState rdstate = PKT_READ_COMPLETE;
332
+
333
+ /* Check if the status code is one which must be handled carefully by the
334
+ * client */
335
+ if (is_fastpath_error(mcresp.status())) {
336
+ // Nothing here!
337
+ } else if (mcresp.status() == PROTOCOL_BINARY_RESPONSE_NOT_MY_VBUCKET) {
228
338
  /* consume the header */
229
339
  DO_ASSIGN_PAYLOAD()
230
340
  if (!handle_nmv(mcresp, request)) {
@@ -232,13 +342,19 @@ Server::try_read(lcbio_CTX *ctx, rdb_IOROPE *ior)
232
342
  }
233
343
  DO_SWALLOW_PAYLOAD()
234
344
  goto GT_DONE;
345
+ } else if (handle_unknown_error(mcresp, err_override)) {
346
+ DO_ASSIGN_PAYLOAD()
347
+ mcreq_dispatch_response(this, request, &mcresp, err_override);
348
+ DO_SWALLOW_PAYLOAD()
349
+ rdstate = PKT_READ_ABORT;
350
+ goto GT_DONE;
235
351
  }
236
352
 
237
353
  /* Figure out if the request is 'ufwd' or not */
238
354
  if (!(request->flags & MCREQ_F_UFWD)) {
239
355
  DO_ASSIGN_PAYLOAD();
240
356
  mcresp.bufh = rdb_get_first_segment(ior);
241
- mcreq_dispatch_response(this, request, &mcresp, LCB_SUCCESS);
357
+ mcreq_dispatch_response(this, request, &mcresp, err_override);
242
358
  DO_SWALLOW_PAYLOAD()
243
359
 
244
360
  } else {
@@ -265,7 +381,7 @@ Server::try_read(lcbio_CTX *ctx, rdb_IOROPE *ior)
265
381
  if (is_last) {
266
382
  mcreq_packet_handled(this, request);
267
383
  }
268
- return PKT_READ_COMPLETE;
384
+ return rdstate;
269
385
  }
270
386
 
271
387
  static void