Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HBASE-11686 Shell code should create a binding / irb workspace instead of polluting the root namespace #2141

Merged
merged 3 commits into from
Jul 28, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 25 additions & 62 deletions bin/hirb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
-h | --help This help.
-n | --noninteractive Do not run within an IRB session and exit with non-zero
status on first error.
--top-level-defs Compatibility flag to export HBase shell commands onto
Ruby's main object
-Dkey=value Pass hbase-*.xml Configuration overrides. For example, to
use an alternate zookeeper ensemble, pass:
-Dhbase.zookeeper.quorum=zookeeper.example.org
Expand All @@ -81,6 +83,7 @@ def add_to_configuration(c, arg)
log_level = org.apache.log4j.Level::ERROR
@shell_debug = false
interactive = true
top_level_definitions = false
_configuration = nil
D_ARG = '-D'.freeze
while (arg = ARGV.shift)
Expand Down Expand Up @@ -108,6 +111,8 @@ def add_to_configuration(c, arg)
warn '[INFO] the -r | --return-values option is ignored. we always behave '\
'as though it was given.'
found.push(arg)
elsif arg == '--top-level-defs'
top_level_definitions = true
else
# Presume it a script. Save it off for running later below
# after we've set up some environment.
Expand Down Expand Up @@ -143,21 +148,10 @@ def add_to_configuration(c, arg)
@shell = Shell::Shell.new(@hbase, interactive)
@shell.debug = @shell_debug

# Add commands to this namespace
# TODO avoid polluting main namespace by using a binding
@shell.export_commands(self)

# Add help command
def help(command = nil)
@shell.help(command)
end

# Backwards compatibility method
def tools
@shell.help_group('tools')
end

# Debugging method
##
# Toggle shell debugging
#
# @return [Boolean] true is debug is turned on after updating the flag
bitoffdev marked this conversation as resolved.
Show resolved Hide resolved
def debug
if @shell_debug
@shell_debug = false
Expand All @@ -173,26 +167,34 @@ def debug
debug?
end

##
# Print whether debug is on or off
def debug?
puts "Debug mode is #{@shell_debug ? 'ON' : 'OFF'}\n\n"
nil
end

# Include hbase constants
include HBaseConstants
# For backwards compatibility, this will export all the HBase shell commands, constants, and
# instance variables (@hbase and @shell) onto Ruby's top-level receiver object known as "main".
@shell.export_all(self) if top_level_definitions

# If script2run, try running it. If we're in interactive mode, will go on to run the shell unless
# script calls 'exit' or 'exit 0' or 'exit errcode'.
load(script2run) if script2run
@shell.eval_io(File.new(script2run)) if script2run

# If we are not running interactively, evaluate standard input
@shell.eval_io(STDIN) unless interactive

if interactive
# Output a banner message that tells users where to go for help
@shell.print_banner

require 'irb'
require 'irb/ext/change-ws'
require 'irb/hirb'

module IRB
# Override of the default IRB.start
def self.start(ap_path = nil)
$0 = File.basename(ap_path, '.rb') if ap_path

Expand All @@ -207,7 +209,12 @@ def self.start(ap_path = nil)
HIRB.new
end

shl = TOPLEVEL_BINDING.receiver.instance_variable_get :'@shell'
hirb.context.change_workspace shl.get_workspace

@CONF[:IRB_RC].call(hirb.context) if @CONF[:IRB_RC]
# Storing our current HBase IRB Context as the main context is imperative for several reasons,
# including auto-completion.
@CONF[:MAIN_CONTEXT] = hirb.context

catch(:IRB_EXIT) do
Expand All @@ -217,48 +224,4 @@ def self.start(ap_path = nil)
end

IRB.start
else
begin
# Noninteractive mode: if there is input on stdin, do a simple REPL.
# XXX Note that this purposefully uses STDIN and not Kernel.gets
# in order to maintain compatibility with previous behavior where
# a user could pass in script2run and then still pipe commands on
# stdin.
require 'irb/ruby-lex'
require 'irb/workspace'
workspace = IRB::WorkSpace.new(binding)
scanner = RubyLex.new

# RubyLex claims to take an IO but really wants an InputMethod
module IOExtensions
def encoding
external_encoding
end
end
IO.include IOExtensions

scanner.set_input(STDIN)
scanner.each_top_level_statement do |statement, linenum|
puts(workspace.evaluate(nil, statement, 'stdin', linenum))
end
# XXX We're catching Exception on purpose, because we want to include
# unwrapped java exceptions, syntax errors, eval failures, etc.
rescue Exception => exception
message = exception.to_s
# exception unwrapping in shell means we'll have to handle Java exceptions
# as a special case in order to format them properly.
if exception.is_a? java.lang.Exception
warn 'java exception'
message = exception.get_message
end
# Include the 'ERROR' string to try to make transition easier for scripts that
# may have already been relying on grepping output.
puts "ERROR #{exception.class}: #{message}"
if $fullBacktrace
# re-raising the will include a backtrace and exit.
raise exception
else
exit 1
end
end
end
3 changes: 2 additions & 1 deletion hbase-shell/src/main/ruby/hbase/admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
java_import org.apache.hadoop.hbase.TableName
java_import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder
java_import org.apache.hadoop.hbase.client.TableDescriptorBuilder
java_import org.apache.hadoop.hbase.HConstants

# Wrapper for org.apache.hadoop.hbase.client.HBaseAdmin

Expand Down Expand Up @@ -1011,7 +1012,7 @@ def cfd(arg, tdb)
cfdb.setTimeToLive(arg.delete(ColumnFamilyDescriptorBuilder::TTL)) if arg.include?(ColumnFamilyDescriptorBuilder::TTL)
cfdb.setDataBlockEncoding(org.apache.hadoop.hbase.io.encoding.DataBlockEncoding.valueOf(arg.delete(ColumnFamilyDescriptorBuilder::DATA_BLOCK_ENCODING))) if arg.include?(ColumnFamilyDescriptorBuilder::DATA_BLOCK_ENCODING)
cfdb.setBlocksize(JInteger.valueOf(arg.delete(ColumnFamilyDescriptorBuilder::BLOCKSIZE))) if arg.include?(ColumnFamilyDescriptorBuilder::BLOCKSIZE)
cfdb.setMaxVersions(JInteger.valueOf(arg.delete(ColumnFamilyDescriptorBuilder::VERSIONS))) if arg.include?(ColumnFamilyDescriptorBuilder::VERSIONS)
cfdb.setMaxVersions(JInteger.valueOf(arg.delete(HConstants::VERSIONS))) if arg.include?(HConstants::VERSIONS)
cfdb.setMinVersions(JInteger.valueOf(arg.delete(ColumnFamilyDescriptorBuilder::MIN_VERSIONS))) if arg.include?(ColumnFamilyDescriptorBuilder::MIN_VERSIONS)
cfdb.setKeepDeletedCells(org.apache.hadoop.hbase.KeepDeletedCells.valueOf(arg.delete(ColumnFamilyDescriptorBuilder::KEEP_DELETED_CELLS).to_s.upcase)) if arg.include?(ColumnFamilyDescriptorBuilder::KEEP_DELETED_CELLS)
cfdb.setCompressTags(JBoolean.valueOf(arg.delete(ColumnFamilyDescriptorBuilder::COMPRESS_TAGS))) if arg.include?(ColumnFamilyDescriptorBuilder::COMPRESS_TAGS)
Expand Down
3 changes: 3 additions & 0 deletions hbase-shell/src/main/ruby/hbase/quotas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ module HBaseQuotasConstants
module Hbase
# rubocop:disable Metrics/ClassLength
class QuotasAdmin
include HBaseConstants
include HBaseQuotasConstants

def initialize(admin)
@admin = admin
end
Expand Down
8 changes: 3 additions & 5 deletions hbase-shell/src/main/ruby/hbase/rsgroup_admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@

module Hbase
class RSGroupAdmin
include HBaseConstants

def initialize(connection)
@connection = connection
@admin = @connection.getAdmin
Expand Down Expand Up @@ -213,11 +211,11 @@ def alter_rsgroup_config(rsgroup_name, *args)
unless arg.is_a?(Hash)
raise(ArgumentError, "#{arg.class} of #{arg.inspect} is not of Hash type")
end
method = arg[METHOD]
method = arg[::HBaseConstants::METHOD]
if method == 'unset'
configuration.remove(arg[NAME])
configuration.remove(arg[::HBaseConstants::NAME])
elsif method == 'set'
arg.delete(METHOD)
arg.delete(::HBaseConstants::METHOD)
for k, v in arg
v = v.to_s unless v.nil?
configuration.put(k, v)
Expand Down
2 changes: 0 additions & 2 deletions hbase-shell/src/main/ruby/hbase/security.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@

module Hbase
class SecurityAdmin
include HBaseConstants

def initialize(admin)
@admin = admin
@connection = @admin.getConnection
Expand Down
2 changes: 0 additions & 2 deletions hbase-shell/src/main/ruby/hbase/taskmonitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@

module Hbase
class TaskMonitor
include HBaseConstants

#---------------------------------------------------------------------------------------------
# Represents information reported by a server on a single MonitoredTask
class Task
Expand Down
4 changes: 1 addition & 3 deletions hbase-shell/src/main/ruby/hbase_constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def self.promote_constants(constants)
promote_constants(org.apache.hadoop.hbase.client.TableDescriptorBuilder.constants)
end

# Include classes definition
# Ensure that hbase class definitions are imported
require 'hbase/hbase'
require 'hbase/admin'
require 'hbase/taskmonitor'
Expand All @@ -126,5 +126,3 @@ def self.promote_constants(constants)
require 'hbase/security'
require 'hbase/visibility_labels'
require 'hbase/rsgroup_admin'

include HBaseQuotasConstants
130 changes: 118 additions & 12 deletions hbase-shell/src/main/ruby/shell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
require 'irb'
require 'irb/workspace'

#
# Simple class to act as the main receiver for an IRB Workspace (and its respective ruby Binding)
# in our HBase shell. This will hold all the commands we want in our shell.
#
class HBaseReceiver < Object
def get_binding
binding
end
end

##
# HBaseIOExtensions is a module to be "mixed-in" (ie. included) to Ruby's IO class. It is required
# if you want to use RubyLex with an IO object. RubyLex claims to take an IO but really wants an
# InputMethod.
module HBaseIOExtensions
def encoding
external_encoding
end
end


# Shell commands module
module Shell
Expand Down Expand Up @@ -115,20 +138,48 @@ def hbase_rsgroup_admin
@rsgroup_admin ||= hbase.rsgroup_admin
end

def export_commands(where)
##
# Create singleton methods on the target receiver object for all the loaded commands
#
# Therefore, export_commands will create "class methods" if passed a Module/Class and if passed
# an instance the methods will not exist on any other instances of the instantiated class.
def export_commands(target)
# We need to store a reference to this Shell instance in the scope of this method so that it
# can be accessed later in the scope of the target object.
shell_inst = self
# Define each method as a lambda. We need to use a lambda (rather than a Proc or block) for
# its properties: preservation of local variables and return
::Shell.commands.keys.each do |cmd|
# here where is the IRB namespace
# this method just adds the call to the specified command
# which just references back to 'this' shell object
# a decently extensible way to add commands
where.send :instance_eval, <<-EOF
def #{cmd}(*args)
ret = @shell.command('#{cmd}', *args)
puts
return ret
end
EOF
target.send :define_singleton_method, cmd.to_sym, lambda { |*args|
ret = shell_inst.command(cmd.to_s, *args)
puts
ret
}
end
# Export help method
target.send :define_singleton_method, :help, lambda { |command = nil|
shell_inst.help(command)
nil
}
# Export tools method for backwards compatibility
target.send :define_singleton_method, :tools, lambda {
shell_inst.help_group('tools')
nil
}
end

# Export HBase commands, constants, and variables to target receiver
def export_all(target)
raise ArgumentError, 'target should not be a module' if target.is_a? Module

# add constants to class of target
target.class.include ::HBaseConstants
target.class.include ::HBaseQuotasConstants
# add instance variables @hbase and @shell for backwards compatibility
target.instance_variable_set :'@hbase', @hbase
target.instance_variable_set :'@shell', self
# add commands to target
export_commands(target)
end

def command_instance(command)
Expand Down Expand Up @@ -238,6 +289,61 @@ def help_footer
For more on the HBase Shell, see http://hbase.apache.org/book.html
HERE
end

@irb_workspace = nil
##
# Returns an IRB Workspace for this shell instance with all the IRB and HBase commands installed
def get_workspace
return @irb_workspace unless @irb_workspace.nil?

hbase_receiver = HBaseReceiver.new
# Install all the hbase commands, constants, and instance variables @shell and @hbase. This
# must be BEFORE the irb commands are installed so that our help command is not overwritten.
export_all(hbase_receiver)
# install all the IRB commands onto our receiver
IRB::ExtendCommandBundle.extend_object(hbase_receiver)
::IRB::WorkSpace.new(hbase_receiver.get_binding)
end

##
# Read from an instance of Ruby's IO class and evaluate each line within the shell's workspace
#
# Unlike Ruby's require or load, this method allows us to execute code with a custom binding. In
# this case, we are using the binding constructed with all the HBase shell constants and
# methods.
def eval_io(io)
require 'irb/ruby-lex'
# Mixing HBaseIOExtensions into IO allows us to pass IO objects to RubyLex.
IO.include HBaseIOExtensions

workspace = get_workspace
scanner = RubyLex.new
scanner.set_input(io)

begin
scanner.each_top_level_statement do |statement, linenum|
puts(workspace.evaluate(nil, statement, 'stdin', linenum))
end
rescue Exception => e
message = e.to_s
# exception unwrapping in shell means we'll have to handle Java exceptions
# as a special case in order to format them properly.
if e.is_a? java.lang.Exception
warn 'java exception'
message = e.get_message
end
# Include the 'ERROR' string to try to make transition easier for scripts that
# may have already been relying on grepping output.
puts "ERROR #{e.class}: #{message}"
if $fullBacktrace
# re-raising the will include a backtrace and exit.
raise e
else
exit 1
end
end
nil
end
end
# rubocop:enable Metrics/ClassLength
end
Expand Down
2 changes: 1 addition & 1 deletion hbase-shell/src/main/ruby/shell/commands/clone_snapshot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def help

def command(snapshot_name, table, args = {})
raise(ArgumentError, 'Arguments should be a Hash') unless args.is_a?(Hash)
restore_acl = args.delete(RESTORE_ACL) || false
restore_acl = args.delete(::HBaseConstants::RESTORE_ACL) || false
admin.clone_snapshot(snapshot_name, table, restore_acl)
end

Expand Down
Loading