marginalia 1.6.0 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: bda214f48d77afb13f9c9182a550272d4e654c4f
4
- data.tar.gz: 5d199a1edd3a808254824a2bb45e22deca536e22
2
+ SHA256:
3
+ metadata.gz: d87c1e1a9f2c0653a979e4bc598c686c8fb7e7af4281f6571c915cb76eee2742
4
+ data.tar.gz: 18b9c7227dc0fedf7fc74954023274484a79a30fe8d9cc47d09c6ecb46010538
5
5
  SHA512:
6
- metadata.gz: 7f7ee0ed6593d2a639c31787a559a86db0367b63ed79dde39966a4264904bbbd77d822e01be212ae40e7a573330281cb75489fc08e577c28e4d059712bcd2a86
7
- data.tar.gz: 4963089d9d9609fd5dbdae17c36e6ed001cea24b81550febdf0826fc3fe13d574f34c8bb51a1ffc6b270e30ba68dd6c14b3d693cec8fcc6bfa4370d8fdee144a
6
+ metadata.gz: 88df0c3b95e116f53a63f6acb1dd6ba63baba048b11e09b47e723d394588c20bab9fadd7035da5a5183fa120fc6871dc2d04205f6397744669f564f684a9f8d5
7
+ data.tar.gz: d91e987e20e1d6a9c257cd5c68b2251b3ced7194f20c38b35e28711088ad030823b0f7018667ce5cb9e02015f472c0fe0a0a34d4ff614dac38194f78e191327b
@@ -1 +1 @@
1
- 2.2.3
1
+ 2.6.6
@@ -1,18 +1,34 @@
1
1
  language: ruby
2
+ sudo: false
3
+
4
+ services:
5
+ - mysql
6
+ - postgresql
2
7
 
3
8
  rvm:
4
- - 2.3.1
5
- - 2.4.4
6
- - 2.5.1
9
+ - 2.2
10
+ - 2.3
11
+ - 2.4
12
+ - 2.5
13
+ - 2.6
14
+ - 2.7
7
15
 
8
- sudo: false
16
+ services:
17
+ - mysql
18
+ - postgresql
19
+
20
+ script: "bundle exec rake db:reset test:all"
9
21
 
10
- script: bundle exec rake db:reset test:all
22
+ gemfile:
23
+ - gemfiles/4.2.gemfile
24
+ - gemfiles/4.2.api.gemfile
25
+ - gemfiles/5.0.gemfile
26
+ - gemfiles/5.1.gemfile
27
+ - gemfiles/5.2.gemfile
11
28
 
12
- env:
13
- - "RAILS_VERSION=4.2.0"
14
- - "RAILS_VERSION=5.0.6"
15
- - "RAILS_VERSION=5.1.5"
16
- - "RAILS_VERSION=4.2.0 TEST_RAILS_API=true"
17
- - "RAILS_VERSION=5.0.6 TEST_RAILS_API=true"
18
- - "RAILS_VERSION=5.1.5 TEST_RAILS_API=true"
29
+ matrix:
30
+ exclude:
31
+ - rvm: 2.7
32
+ gemfile: gemfiles/4.2.gemfile
33
+ - rvm: 2.7
34
+ gemfile: gemfiles/4.2.api.gemfile
data/Gemfile CHANGED
@@ -10,7 +10,7 @@ else
10
10
  gem 'mysql2', '>= 0.3.13', '< 0.5'
11
11
  end
12
12
  gem 'pg', '~> 0.15'
13
- gem 'sqlite3'
13
+ gem 'sqlite3', '~> 1.3.6'
14
14
 
15
15
  rails = case version
16
16
  when "master"
@@ -24,3 +24,7 @@ gem "rails", rails
24
24
  if ENV["TEST_RAILS_API"] == "true"
25
25
  gem "rails-api", "~> 0.2.1"
26
26
  end
27
+
28
+ if RUBY_VERSION.start_with?('2.3')
29
+ gem 'mysql'
30
+ end
data/README.md CHANGED
@@ -56,6 +56,8 @@ Optionally, you can set the application name shown in the log like so in an init
56
56
  For Rails 3 applications, the name will default to your Rails application name.
57
57
  For Rails 2 applications, "rails" is used as the default application name.
58
58
 
59
+ #### Components
60
+
59
61
  You can also configure the components of the comment that will be appended,
60
62
  by setting `Marginalia::Comment.components`. By default, this is set to:
61
63
 
@@ -100,6 +102,37 @@ With ActiveRecord >= 3.2.19:
100
102
 
101
103
  Pull requests for other included comment components are welcome.
102
104
 
105
+ #### Prepend comments
106
+
107
+ By default marginalia appends the comments at the end of the query. Certain databases, such as MySQL will truncate
108
+ the query text. This is the case for slow query logs and the results of querying some InnoDB internal tables where the
109
+ length of the query is more than 1024 bytes.
110
+
111
+ In order to not lose the marginalia comments from your logs, you can prepend the comments using this option:
112
+
113
+ Marginalia::Comment.prepend_comment = true
114
+
115
+ #### Inline query annotations
116
+
117
+ In addition to the request or job-level component-based annotations,
118
+ Marginalia may be used to add inline annotations to specific queries using a
119
+ block-based API.
120
+
121
+ For example, the following code:
122
+
123
+ Marginalia.with_annotation("foo") do
124
+ Account.where(queenbee_id: 1234567890).first
125
+ end
126
+
127
+ will issue this query:
128
+
129
+ Account Load (0.3ms) SELECT `accounts`.* FROM `accounts`
130
+ WHERE `accounts`.`queenbee_id` = 1234567890
131
+ LIMIT 1
132
+ /*application:BCX,controller:project_imports,action:show*/ /*foo*/
133
+
134
+ Nesting `with_annotation` blocks will concatenate the comment strings.
135
+
103
136
  ## Contributing
104
137
 
105
138
  Start by bundling and creating the test database:
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "mysql2", "~> 0.3.13"
4
+ gem "pg", "~> 0.15"
5
+ gem "sqlite3", "~> 1.3.6"
6
+ gem "rails", "= 4.2.11.1"
7
+ gem "rails-api", "~> 0.2.1"
8
+
9
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "mysql2", "~> 0.3.13"
4
+ gem "pg", "~> 0.15"
5
+ gem "sqlite3", "~> 1.3.6"
6
+ gem "rails", "= 4.2.11.1"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "mysql2", "~> 0.3.13"
4
+ gem "pg", "~> 0.15"
5
+ gem "sqlite3", "~> 1.3.6"
6
+ gem "rails", "= 5.0.7.2"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "mysql2", "~> 0.3.13"
4
+ gem "pg", "~> 0.15"
5
+ gem "sqlite3", "~> 1.3.6"
6
+ gem "rails", "= 5.1.6.2"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "mysql2", "~> 0.4.10"
4
+ gem "pg", "~> 0.15"
5
+ gem "sqlite3", "~> 1.3.6"
6
+ gem "rails", "= 5.2.2.1"
7
+
8
+ gemspec :path => "../"
@@ -1,5 +1,6 @@
1
1
  require 'marginalia/railtie'
2
2
  require 'marginalia/comment'
3
+ require 'marginalia/sidekiq_instrumentation'
3
4
 
4
5
  module Marginalia
5
6
  mattr_accessor :application_name
@@ -48,10 +49,22 @@ module Marginalia
48
49
  Marginalia::Comment.update_adapter!(self)
49
50
  comment = Marginalia::Comment.construct_comment
50
51
  if comment.present? && !sql.include?(comment)
51
- "#{sql} /*#{comment}*/"
52
- else
53
- sql
52
+ sql = if Marginalia::Comment.prepend_comment
53
+ "/*#{comment}*/ #{sql}"
54
+ else
55
+ "#{sql} /*#{comment}*/"
56
+ end
57
+ end
58
+ inline_comment = Marginalia::Comment.construct_inline_comment
59
+ if inline_comment.present? && !sql.include?(inline_comment)
60
+ sql = if Marginalia::Comment.prepend_comment
61
+ "/*#{inline_comment}*/ #{sql}"
62
+ else
63
+ "#{sql} /*#{inline_comment}*/"
64
+ end
54
65
  end
66
+
67
+ sql
55
68
  end
56
69
 
57
70
  def execute_with_marginalia(sql, name = nil)
@@ -63,9 +76,9 @@ module Marginalia
63
76
  end
64
77
 
65
78
  if ActiveRecord::VERSION::MAJOR >= 5
66
- def exec_query_with_marginalia(sql, name = 'SQL', binds = [], options = {})
79
+ def exec_query_with_marginalia(sql, name = 'SQL', binds = [], **options)
67
80
  options[:prepare] ||= false
68
- exec_query_without_marginalia(annotate_sql(sql), name, binds, options)
81
+ exec_query_without_marginalia(annotate_sql(sql), name, binds, **options)
69
82
  end
70
83
  end
71
84
 
@@ -77,9 +90,15 @@ module Marginalia
77
90
  exec_update_without_marginalia(annotate_sql(sql), name, binds)
78
91
  end
79
92
 
80
- def execute_and_clear_with_marginalia(sql, *args, &block)
81
- execute_and_clear_without_marginalia(annotate_sql(sql), *args, &block)
93
+ if ActiveRecord::VERSION::MAJOR >= 5
94
+ def execute_and_clear_with_marginalia(sql, *args, **kwargs, &block)
95
+ execute_and_clear_without_marginalia(annotate_sql(sql), *args, **kwargs, &block)
82
96
  end
97
+ else
98
+ def execute_and_clear_with_marginalia(sql, *args, &block)
99
+ execute_and_clear_without_marginalia(annotate_sql(sql), *args, &block)
100
+ end
101
+ end
83
102
  end
84
103
 
85
104
  module ActionControllerInstrumentation
@@ -100,4 +119,11 @@ module Marginalia
100
119
  Marginalia::Comment.clear!
101
120
  end
102
121
  end
122
+
123
+ def self.with_annotation(comment, &block)
124
+ Marginalia::Comment.inline_annotations.push(comment)
125
+ block.call if block.present?
126
+ ensure
127
+ Marginalia::Comment.inline_annotations.pop
128
+ end
103
129
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'socket'
2
4
 
3
5
  module Marginalia
4
6
  module Comment
5
- mattr_accessor :components, :lines_to_ignore
7
+ mattr_accessor :components, :lines_to_ignore, :prepend_comment
6
8
  Marginalia::Comment.components ||= [:application, :controller, :action]
7
9
 
8
10
  def self.update!(controller = nil)
@@ -18,11 +20,11 @@ module Marginalia
18
20
  end
19
21
 
20
22
  def self.construct_comment
21
- ret = ''
23
+ ret = String.new
22
24
  self.components.each do |c|
23
25
  component_value = self.send(c)
24
26
  if component_value.present?
25
- ret << "#{c.to_s}:#{component_value.to_s},"
27
+ ret << "#{c}:#{component_value},"
26
28
  end
27
29
  end
28
30
  ret.chop!
@@ -30,6 +32,11 @@ module Marginalia
30
32
  ret
31
33
  end
32
34
 
35
+ def self.construct_inline_comment
36
+ return nil if inline_annotations.none?
37
+ escape_sql_comment(inline_annotations.join)
38
+ end
39
+
33
40
  def self.escape_sql_comment(str)
34
41
  while str.include?('/*') || str.include?('*/')
35
42
  str = str.gsub('/*', '').gsub('*/', '')
@@ -96,8 +103,15 @@ module Marginalia
96
103
  marginalia_controller.action_name if marginalia_controller.respond_to? :action_name
97
104
  end
98
105
 
106
+ def self.sidekiq_job
107
+ marginalia_job["class"] if marginalia_job && marginalia_job.respond_to?(:[])
108
+ end
109
+
110
+ DEFAULT_LINES_TO_IGNORE_REGEX = %r{\.rvm|/ruby/gems/|vendor/|marginalia|rbenv|monitor\.rb.*mon_synchronize}
111
+
99
112
  def self.line
100
- Marginalia::Comment.lines_to_ignore ||= /\.rvm|gem|vendor\/|marginalia|rbenv/
113
+ Marginalia::Comment.lines_to_ignore ||= DEFAULT_LINES_TO_IGNORE_REGEX
114
+
101
115
  last_line = caller.detect do |line|
102
116
  line !~ Marginalia::Comment.lines_to_ignore
103
117
  end
@@ -154,6 +168,10 @@ module Marginalia
154
168
  marginalia_adapter.pool.spec.config
155
169
  end
156
170
  end
171
+
172
+ def self.inline_annotations
173
+ Thread.current[:marginalia_inline_annotations] ||= []
174
+ end
157
175
  end
158
176
 
159
177
  end
@@ -0,0 +1,25 @@
1
+ module Marginalia
2
+
3
+ # Alternative to ActiveJob Instrumentation for Sidekiq.
4
+ # Apt for Instrumenting Sidekiq with Rails version < 4.2.
5
+ module SidekiqInstrumentation
6
+
7
+ class Middleware
8
+ def call(worker, msg, queue)
9
+ Marginalia::Comment.update_job! msg
10
+ yield
11
+ ensure
12
+ Marginalia::Comment.clear_job!
13
+ end
14
+ end
15
+
16
+ def self.enable!
17
+ Sidekiq.configure_server do |config|
18
+ config.server_middleware do |chain|
19
+ chain.add Marginalia::SidekiqInstrumentation::Middleware
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.authors = ["Noah Lorang", "Nick Quaranto", "Taylor Weibley"]
3
- gem.email = ["noah@37signals.com", "arthurnn@github.com"]
3
+ gem.email = ["arthurnn@github.com"]
4
4
  gem.homepage = "https://github.com/basecamp/marginalia"
5
5
 
6
6
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -8,7 +8,7 @@ Gem::Specification.new do |gem|
8
8
  gem.test_files = `git ls-files -- {test}/*`.split("\n")
9
9
  gem.name = "marginalia"
10
10
  gem.require_paths = ["lib"]
11
- gem.version = "1.6.0"
11
+ gem.version = "1.10.0"
12
12
  gem.license = "MIT"
13
13
 
14
14
  gem.add_runtime_dependency "actionpack", ">= 2.3"
@@ -19,8 +19,7 @@ Gem::Specification.new do |gem|
19
19
  gem.add_development_dependency "sqlite3"
20
20
  gem.add_development_dependency "minitest"
21
21
  gem.add_development_dependency "mocha"
22
+ gem.add_development_dependency "sidekiq"
22
23
 
23
24
  gem.summary = gem.description = %q{Attach comments to your ActiveRecord queries.}
24
-
25
- gem.extensions = ["ext/mkrf_conf.rb"]
26
25
  end
@@ -23,6 +23,8 @@ require 'logger'
23
23
  require 'pp'
24
24
  require 'active_record'
25
25
  require 'action_controller'
26
+ require 'sidekiq'
27
+ require 'sidekiq/testing'
26
28
 
27
29
  if request_id_available?
28
30
  require 'action_dispatch/middleware/request_id'
@@ -87,6 +89,13 @@ if active_job_available?
87
89
  end
88
90
  end
89
91
 
92
+ class PostsSidekiqJob
93
+ include Sidekiq::Worker
94
+ def perform
95
+ Post.first
96
+ end
97
+ end
98
+
90
99
  if using_rails_api?
91
100
  class PostsApiController < ActionController::API
92
101
  def driver_only
@@ -112,6 +121,7 @@ class MarginaliaTest < MiniTest::Test
112
121
  @queries << args.last[:sql]
113
122
  end
114
123
  @env = Rack::MockRequest.env_for('/')
124
+ ActiveJob::Base.queue_adapter = :inline
115
125
  end
116
126
 
117
127
  def test_double_annotate
@@ -206,6 +216,16 @@ class MarginaliaTest < MiniTest::Test
206
216
  assert_match %r{/\*line:.*lib/marginalia/comment.rb:[0-9]+}, @queries.first
207
217
  end
208
218
 
219
+ def test_default_lines_to_ignore_regex
220
+ line = "/gems/a_gem/lib/a_gem.rb:1:in `some_method'"
221
+ call_stack = [line] + caller
222
+
223
+ assert_match(
224
+ call_stack.detect { |line| line !~ Marginalia::Comment::DEFAULT_LINES_TO_IGNORE_REGEX },
225
+ line
226
+ )
227
+ end
228
+
209
229
  def test_hostname_and_pid
210
230
  Marginalia::Comment.components = [:hostname, :pid]
211
231
  PostsController.action(:driver_only).call(@env)
@@ -273,6 +293,33 @@ class MarginaliaTest < MiniTest::Test
273
293
  Post.first
274
294
  refute_match %{job:PostsJob}, @queries.last
275
295
  end
296
+
297
+ def test_active_job_with_sidekiq
298
+ Marginalia::Comment.components = [:job, :sidekiq_job]
299
+ PostsJob.perform_later
300
+ assert_match %{job:PostsJob}, @queries.first
301
+
302
+ Post.first
303
+ refute_match %{job:PostsJob}, @queries.last
304
+ end
305
+ end
306
+
307
+ def test_sidekiq_job
308
+ Marginalia::Comment.components = [:sidekiq_job]
309
+ Marginalia::SidekiqInstrumentation.enable!
310
+
311
+ # Test harness does not run Sidekiq middlewares by default so include testing middleware.
312
+ Sidekiq::Testing.server_middleware do |chain|
313
+ chain.add Marginalia::SidekiqInstrumentation::Middleware
314
+ end
315
+
316
+ Sidekiq::Testing.fake!
317
+ PostsSidekiqJob.perform_async
318
+ PostsSidekiqJob.drain
319
+ assert_match %{sidekiq_job:PostsSidekiqJob}, @queries.first
320
+
321
+ Post.first
322
+ refute_match %{sidekiq_job:PostsSidekiqJob}, @queries.last
276
323
  end
277
324
 
278
325
  def test_good_comment
@@ -284,6 +331,53 @@ class MarginaliaTest < MiniTest::Test
284
331
  assert_equal Marginalia::Comment.escape_sql_comment('**//; DROP TABLE USERS;/*'), '; DROP TABLE USERS;'
285
332
  end
286
333
 
334
+ def test_inline_annotations
335
+ Marginalia.with_annotation("foo") do
336
+ Post.first
337
+ end
338
+ Post.first
339
+ assert_match %r{/\*foo\*/$}, @queries.first
340
+ refute_match %r{/\*foo\*/$}, @queries.last
341
+ # Assert we're not adding an empty comment, either
342
+ refute_match %r{/\*\s*\*/$}, @queries.last
343
+ end
344
+
345
+ def test_nested_inline_annotations
346
+ Marginalia.with_annotation("foo") do
347
+ Marginalia.with_annotation("bar") do
348
+ Post.first
349
+ end
350
+ end
351
+ assert_match %r{/\*foobar\*/$}, @queries.first
352
+ end
353
+
354
+ def test_bad_inline_annotations
355
+ Marginalia.with_annotation("*/; DROP TABLE USERS;/*") do
356
+ Post.first
357
+ end
358
+ Marginalia.with_annotation("**//; DROP TABLE USERS;//**") do
359
+ Post.first
360
+ end
361
+ assert_match %r{/\*; DROP TABLE USERS;\*/$}, @queries.first
362
+ assert_match %r{/\*; DROP TABLE USERS;\*/$}, @queries.last
363
+ end
364
+
365
+ def test_inline_annotations_are_deduped
366
+ Marginalia.with_annotation("foo") do
367
+ ActiveRecord::Base.connection.execute "select id from posts /*foo*/"
368
+ end
369
+ assert_match %r{select id from posts /\*foo\*/ /\*application:rails\*/$}, @queries.first
370
+ end
371
+
372
+ def test_add_comments_to_beginning_of_query
373
+ Marginalia::Comment.prepend_comment = true
374
+
375
+ ActiveRecord::Base.connection.execute "select id from posts"
376
+ assert_match %r{/\*application:rails\*/ select id from posts$}, @queries.first
377
+ ensure
378
+ Marginalia::Comment.prepend_comment = nil
379
+ end
380
+
287
381
  def teardown
288
382
  Marginalia.application_name = nil
289
383
  Marginalia::Comment.lines_to_ignore = nil
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: marginalia
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Lorang
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2018-09-24 00:00:00.000000000 Z
13
+ date: 2021-01-04 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: actionpack
@@ -124,13 +124,25 @@ dependencies:
124
124
  - - ">="
125
125
  - !ruby/object:Gem::Version
126
126
  version: '0'
127
+ - !ruby/object:Gem::Dependency
128
+ name: sidekiq
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
127
141
  description: Attach comments to your ActiveRecord queries.
128
142
  email:
129
- - noah@37signals.com
130
143
  - arthurnn@github.com
131
144
  executables: []
132
- extensions:
133
- - ext/mkrf_conf.rb
145
+ extensions: []
134
146
  extra_rdoc_files: []
135
147
  files:
136
148
  - ".gitignore"
@@ -140,11 +152,16 @@ files:
140
152
  - LICENSE
141
153
  - README.md
142
154
  - Rakefile
143
- - ext/mkrf_conf.rb
155
+ - gemfiles/4.2.api.gemfile
156
+ - gemfiles/4.2.gemfile
157
+ - gemfiles/5.0.gemfile
158
+ - gemfiles/5.1.gemfile
159
+ - gemfiles/5.2.gemfile
144
160
  - init.rb
145
161
  - lib/marginalia.rb
146
162
  - lib/marginalia/comment.rb
147
163
  - lib/marginalia/railtie.rb
164
+ - lib/marginalia/sidekiq_instrumentation.rb
148
165
  - marginalia.gemspec
149
166
  - test/query_comments_test.rb
150
167
  homepage: https://github.com/basecamp/marginalia
@@ -166,8 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
166
183
  - !ruby/object:Gem::Version
167
184
  version: '0'
168
185
  requirements: []
169
- rubyforge_project:
170
- rubygems_version: 2.4.5.1
186
+ rubygems_version: 3.0.3
171
187
  signing_key:
172
188
  specification_version: 4
173
189
  summary: Attach comments to your ActiveRecord queries.
@@ -1,21 +0,0 @@
1
- require 'rubygems'
2
- require 'rubygems/command.rb'
3
- require 'rubygems/dependency_installer.rb'
4
-
5
- begin
6
- Gem::Command.build_args = ARGV
7
- rescue NoMethodError
8
- end
9
-
10
- installer = Gem::DependencyInstaller.new
11
- begin
12
- if RUBY_VERSION < "2.4"
13
- installer.install "mysql", ">=0"
14
- end
15
- rescue
16
- exit(1)
17
- end
18
-
19
- f = File.open(File.join(File.dirname(__FILE__), "Rakefile"), "w")
20
- f.write("task :default\n")
21
- f.close